Merge "Issue #2421 optimize color map loading. Change-Id: I871ddf0f3eabee0e27d858e2cfa897563875c304" into development

Former-commit-id: eeee5c7948 [formerly eeee5c7948 [formerly 7139b6fe32b6de8c87c96e3b55d6ad83f11b149a]]
Former-commit-id: c01eb85e53
Former-commit-id: 72a6650958
This commit is contained in:
Lee Venable 2013-10-07 13:56:50 -05:00 committed by Gerrit Code Review
commit f34b07d207
4 changed files with 555 additions and 204 deletions

View file

@ -48,13 +48,15 @@ import com.raytheon.uf.viz.core.exception.VizException;
* <pre>
* SOFTWARE HISTORY
* Date Ticket# Engineer Description
* ------------ ---------- ----------- --------------------------
* ------------- -------- ----------- --------------------------
* Feb 13, 2007 chammack Initial Creation.
* Aug 20, 2007 njensen Added listColorMaps().
* Aug 20, 2008 dglazesk JiBX to JaXB
* Aug 20, 2008 dglazesk Updated for new ColorMap interface
* Jun 10, 2013 2075 njensen Added listColorMapFiles(subdirectory)
* Aug 06, 2013 2210 njensen Moved colormaps to common_static
* Sep 18, 2013 2421 bsteffen Moved some listing capabilities into
* ColorMapTree.
*
* </pre>
*
@ -64,9 +66,9 @@ import com.raytheon.uf.viz.core.exception.VizException;
public class ColorMapLoader {
private static final String EXTENSION = ".cmap";
public static final String EXTENSION = ".cmap";
private static final String DIR_NAME = "colormaps";
public static final String DIR_NAME = "colormaps";
private static final String sharedMutex = "";
@ -227,15 +229,6 @@ public class ColorMapLoader {
+ subDirectory);
}
/**
* Lists all the colormaps found in the system
*
* @return
*/
public static LocalizationFile[] listColorMapFiles() {
return internalListColorMapFiles(DIR_NAME);
}
public static String shortenName(LocalizationFile file) {
String name = file.getName()
.replace(DIR_NAME + IPathManager.SEPARATOR, "")
@ -258,7 +251,7 @@ public class ColorMapLoader {
*/
public static String[] listColorMaps(
LocalizationContext.LocalizationLevel aType) {
LocalizationFile[] files = listColorMapFiles();
LocalizationFile[] files = internalListColorMapFiles(DIR_NAME);
String[] cmaps = new String[files.length];
for (int i = 0; i < files.length; i++) {
cmaps[i] = shortenName(files[i]);

View file

@ -0,0 +1,267 @@
/**
* This software was developed and / or modified by Raytheon Company,
* pursuant to Contract DG133W-05-CQ-1067 with the US Government.
*
* U.S. EXPORT CONTROLLED TECHNICAL DATA
* This software product contains export-restricted data whose
* export/transfer/disclosure is restricted by U.S. law. Dissemination
* to non-U.S. persons whether in the United States or abroad requires
* an export license or other authorization.
*
* Contractor Name: Raytheon Company
* Contractor Address: 6825 Pine Street, Suite 340
* Mail Stop B8
* Omaha, NE 68106
* 402.291.0100
*
* See the AWIPS II Master Rights File ("Master Rights File.pdf") for
* further licensing information.
**/
package com.raytheon.uf.viz.core.drawables;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import com.raytheon.uf.common.localization.FileUpdatedMessage;
import com.raytheon.uf.common.localization.ILocalizationFileObserver;
import com.raytheon.uf.common.localization.IPathManager;
import com.raytheon.uf.common.localization.LocalizationContext;
import com.raytheon.uf.common.localization.LocalizationContext.LocalizationLevel;
import com.raytheon.uf.common.localization.LocalizationContext.LocalizationType;
import com.raytheon.uf.common.localization.LocalizationFile;
import com.raytheon.uf.common.localization.LocalizationNotificationObserver;
/**
* ColorMapTree represents the directory structure of colormaps directory. The
* levels of a Tree can represent a {@link LocalizationLevel}, a
* {@link LocalizationContext} or a localization directory.
*
* <pre>
*
* SOFTWARE HISTORY
*
* Date Ticket# Engineer Description
* ------------- -------- ----------- --------------------------
* Sep 18, 2013 2421 bsteffen Initial creation
*
* </pre>
*
* @author bsteffen
* @version 1.0
*/
public class ColorMapTree {
private final String path;
private final IPathManager pathManager;
private final LocalizationLevel level;
private final LocalizationContext context;
private final Object filesLock = new Object();
private LocalizationFile[] files;
private final Object subTreesLock = new Object();
private List<ColorMapTree> subTrees;
/**
* Create a tree for the given path and context. The tree will represent the
* colormap files that exist at the path within the context.
*/
public ColorMapTree(IPathManager pathManager, LocalizationContext context,
String path) {
this.path = path;
this.pathManager = pathManager;
this.level = null;
this.context = context;
}
/**
* Create a tree for the given path and level. The tree will have the same
* name as the level and will have a subtree for each context that exists at
* that level. Each context tree will represent the colormap files that
* exist at the path.
*/
public ColorMapTree(IPathManager pathManager, LocalizationLevel level,
String path) {
this.path = path;
this.pathManager = pathManager;
this.level = level;
this.context = null;
LocalizationNotificationObserver.getInstance()
.addGlobalFileChangeObserver(new FileChangeListener(this));
}
/**
* For a tree based only on a {@link LocalizationLevel} this returns the
* name of the level, for a tree at the root directory of a context this
* will be the name of the {@link LocalizationContext} and for a tree
* representing a subdirectory this will be the directory name.
*/
public String getName() {
if (context == null) {
return level.name();
} else {
int start = path.lastIndexOf(IPathManager.SEPARATOR);
if (start <= 0) {
return context.getContextName();
}
return path.substring(start + 1);
}
}
/**
* For a tree based on a {@link LocalizationLevel} this returns a tree for
* each context at the level. Otherwise it returns a tree for each
* subdirectory of this tree.
*/
public List<ColorMapTree> getSubTrees() {
synchronized (subTreesLock) {
if (subTrees == null) {
subTrees = new ArrayList<ColorMapTree>();
if (context == null) {
for (String context : pathManager.getContextList(level)) {
LocalizationContext ctx = pathManager.getContext(
LocalizationType.COMMON_STATIC, level);
ctx.setContextName(context);
subTrees.add(new ColorMapTree(pathManager, ctx, path));
}
} else {
for (LocalizationFile file : requestFiles()) {
if (file.isDirectory() && !path.equals(file.getName())) {
subTrees.add(new ColorMapTree(pathManager, context,
file.getName()));
}
}
}
}
return new ArrayList<ColorMapTree>(subTrees);
}
}
/**
*
* @return all color map files within this level of the tree.
*/
public List<LocalizationFile> getColorMapFiles() {
if (context == null) {
return Collections.emptyList();
} else {
List<LocalizationFile> result = new ArrayList<LocalizationFile>();
for (LocalizationFile file : requestFiles()) {
if (!file.isDirectory()) {
result.add(file);
}
}
return result;
}
}
/**
*
* @return true if this tree does not contain any color map files or any
* subtrees which contain color map files(recursively).
*/
public boolean isEmpty() {
if (getColorMapFiles().isEmpty()) {
for (ColorMapTree tree : getSubTrees()) {
if (!tree.isEmpty()) {
return false;
}
}
return true;
}
return false;
}
/**
* Optimize the internal structure so future {@link #isEmpty()} calls are
* fast. isEmpty() is a slow operations on trees with many empty subtrees,
* so this can be called in the background to enable faster calls to isEmpty
* when it is needed. In cases where isEmpty does not need extra data or is
* already optimized this call should complete very quickly.
*/
public void optimizeIsEmpty() {
/* isEmpty caches data in the subtrees so nothing else is needed. */
isEmpty();
}
/**
* This method will receive a message for every localization file in all
* contexts. It must filter by path and by level and/or context.
*/
protected void handleUpdate(FileUpdatedMessage message) {
if (message.getFileName().startsWith(path)) {
LocalizationContext context = message.getContext();
if (context.getLocalizationLevel().equals(level)) {
synchronized (subTreesLock) {
if (subTrees != null) {
for (ColorMapTree subTree : subTrees) {
if (subTree.getName().equals(
context.getContextName())) {
subTree.handleUpdate(message);
return;
}
}
subTrees.add(new ColorMapTree(pathManager, context,
path));
}
}
} else if (context.equals(context)) {
synchronized (filesLock) {
files = null;
}
synchronized (subTreesLock) {
subTrees = null;
}
}
}
}
private LocalizationFile[] requestFiles() {
synchronized (filesLock) {
if (files == null) {
files = pathManager
.listFiles(context, path,
new String[] { ColorMapLoader.EXTENSION },
false, false);
}
return files;
}
}
/**
* {@link WeakReference} based listener which automatically removes itself
* when a notification arrives and the {@link ColorMapTree} has been garbage
* collected.
*/
private static class FileChangeListener implements
ILocalizationFileObserver {
private final Reference<ColorMapTree> treeRef;
private FileChangeListener(ColorMapTree tree) {
treeRef = new WeakReference<ColorMapTree>(tree);
}
@Override
public void fileUpdated(FileUpdatedMessage message) {
ColorMapTree tree = treeRef.get();
if (tree == null) {
LocalizationNotificationObserver.getInstance()
.removeGlobalFileChangeObserver(this);
} else {
tree.handleUpdate(message);
}
}
}
}

View file

@ -0,0 +1,95 @@
/**
* This software was developed and / or modified by Raytheon Company,
* pursuant to Contract DG133W-05-CQ-1067 with the US Government.
*
* U.S. EXPORT CONTROLLED TECHNICAL DATA
* This software product contains export-restricted data whose
* export/transfer/disclosure is restricted by U.S. law. Dissemination
* to non-U.S. persons whether in the United States or abroad requires
* an export license or other authorization.
*
* Contractor Name: Raytheon Company
* Contractor Address: 6825 Pine Street, Suite 340
* Mail Stop B8
* Omaha, NE 68106
* 402.291.0100
*
* See the AWIPS II Master Rights File ("Master Rights File.pdf") for
* further licensing information.
**/
package com.raytheon.uf.viz.core.drawables;
import java.util.HashMap;
import java.util.Map;
import com.raytheon.uf.common.localization.IPathManager;
import com.raytheon.uf.common.localization.LocalizationContext;
import com.raytheon.uf.common.localization.LocalizationContext.LocalizationLevel;
import com.raytheon.uf.common.localization.LocalizationContext.LocalizationType;
import com.raytheon.uf.common.localization.PathManagerFactory;
/**
* Factory which can provide cached versions of {@link ColorMapTree} objects.
*
* <pre>
*
* SOFTWARE HISTORY
*
* Date Ticket# Engineer Description
* ------------- -------- ----------- --------------------------
* Sep 18, 2013 2421 bsteffen Initial creation
*
* </pre>
*
* @author bsteffen
* @version 1.0
*/
public class ColorMapTreeFactory {
private static ColorMapTree baseTree;
private static Object baseTreeLock = new Object();
private static final Map<LocalizationLevel, ColorMapTree> treesByLevel = new HashMap<LocalizationLevel, ColorMapTree>();
private static Object treesByLevelLock = new Object();
/**
* Get a tree for the BASE localization context. This tree will be different
* from the tree returned by getTreeForLevel(LocalizationLevel.BASE) because
* it will not be for the BASE level but instead is for the BASE context.
*
*/
public static ColorMapTree getBaseTree() {
synchronized (baseTreeLock) {
if(baseTree == null){
IPathManager pm = PathManagerFactory.getPathManager();
LocalizationContext baseContext = pm.getContext(
LocalizationType.COMMON_STATIC, LocalizationLevel.BASE);
baseTree = new ColorMapTree(pm, baseContext,
ColorMapLoader.DIR_NAME);
}
return baseTree;
}
}
/**
* Return a {@link ColorMapTree}Tree for the provided level. The tree will
* have the same name as the level and will have a subtree for each context
* that exists at that level.
*/
public static ColorMapTree getTreeForLevel(LocalizationLevel level) {
synchronized (treesByLevelLock) {
ColorMapTree tree = treesByLevel.get(level);
if (tree == null) {
IPathManager pm = PathManagerFactory.getPathManager();
tree = new ColorMapTree(pm, level, ColorMapLoader.DIR_NAME);
treesByLevel.put(level, tree);
}
return tree;
}
}
}

View file

@ -19,17 +19,20 @@
**/
package com.raytheon.viz.ui.dialogs;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.MenuEvent;
import org.eclipse.swt.events.MenuListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Point;
@ -41,8 +44,17 @@ import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Shell;
import com.raytheon.uf.common.colormap.prefs.ColorMapParameters;
import com.raytheon.uf.common.localization.IPathManager;
import com.raytheon.uf.common.localization.LocalizationContext.LocalizationLevel;
import com.raytheon.uf.common.localization.LocalizationFile;
import com.raytheon.uf.common.localization.PathManager;
import com.raytheon.uf.common.localization.PathManagerFactory;
import com.raytheon.uf.common.status.IUFStatusHandler;
import com.raytheon.uf.common.status.UFStatus;
import com.raytheon.uf.common.status.UFStatus.Priority;
import com.raytheon.uf.viz.core.drawables.ColorMapLoader;
import com.raytheon.uf.viz.core.drawables.ColorMapTree;
import com.raytheon.uf.viz.core.drawables.ColorMapTreeFactory;
import com.raytheon.uf.viz.core.exception.VizException;
import com.raytheon.uf.viz.core.rsc.capabilities.ColorMapCapability;
@ -53,8 +65,9 @@ import com.raytheon.uf.viz.core.rsc.capabilities.ColorMapCapability;
*
* SOFTWARE HISTORY
* Date Ticket# Engineer Description
* ------------ ---------- ----------- --------------------------
* ------------- -------- ----------- --------------------------
* Jul 26, 2010 mschenke Initial creation
* Sep 18, 2013 2421 bsteffen Use ColorMapTree for asyncronous loading.
*
* </pre>
*
@ -64,88 +77,15 @@ import com.raytheon.uf.viz.core.rsc.capabilities.ColorMapCapability;
public class ColormapComp {
private static final transient IUFStatusHandler statusHandler = UFStatus
.getHandler(ColormapComp.class);
public static interface IColormapCompChangeListener {
public void colormapChanged(String colorMap);
}
private class ColorMapMenuItem {
/** null if not leaf node */
private LocalizationFile cmapFile;
private Menu subMenu;
private MenuItem actualItem;
private Menu parent;
private String text;
/**
* @param parent
* @param style
*/
public ColorMapMenuItem(Menu parent, Menu subMenu,
LocalizationFile cmapFile, String text) {
this.parent = parent;
this.subMenu = subMenu;
this.cmapFile = cmapFile;
this.text = text;
List<ColorMapMenuItem> children = childMap.get(parent);
if (children == null) {
children = new ArrayList<ColorMapMenuItem>();
childMap.put(parent, children);
}
children.add(this);
}
public void initializeComponents() {
actualItem = new MenuItem(parent, subMenu == null ? SWT.NONE
: SWT.CASCADE);
actualItem.setText(text);
if (subMenu != null) {
actualItem.setMenu(subMenu);
}
actualItem.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
if (cmapFile != null) {
if (cmapButton != null) {
cmapButton.setText(cmapFile.getName()
.replace("colormaps" + File.separator, "")
.replace(".cmap", ""));
}
try {
String shortName = ColorMapLoader
.shortenName(cmapFile);
params.setColorMap(ColorMapLoader
.loadColorMap(shortName));
cap.notifyResources();
for (IColormapCompChangeListener listener : listeners) {
listener.colormapChanged(shortName);
}
} catch (VizException e1) {
e1.printStackTrace();
}
}
}
});
}
public Menu getSubMenu() {
return subMenu;
}
}
private Set<IColormapCompChangeListener> listeners = new HashSet<IColormapCompChangeListener>();
private Map<Menu, List<ColorMapMenuItem>> childMap = new HashMap<Menu, List<ColorMapMenuItem>>();
private Button cmapButton;
private Shell shell;
@ -211,54 +151,8 @@ public class ColormapComp {
}
cmapPopupMenu.setVisible(false);
LocalizationFile[] files = ColorMapLoader.listColorMapFiles();
for (LocalizationFile file : files) {
List<String> actualItems = new ArrayList<String>();
String[] split = ColorMapLoader.shortenName(file).split("[/\\\\]"); // Win32
Menu lastParent = cmapPopupMenu;
// Just to be sure, get rid of empty items
for (String text : split) {
if (text.trim().equals("") == false) {
actualItems.add(text);
}
}
for (int i = 0; i < actualItems.size(); ++i) {
if (i == actualItems.size() - 1) {
// leaf node
new ColorMapMenuItem(lastParent, null, file,
actualItems.get(i));
} else {
String text = actualItems.get(i);
boolean found = false;
// traverse lastParent's children to see if we already exist
List<ColorMapMenuItem> children = childMap.get(lastParent);
if (children == null) {
children = new ArrayList<ColorMapMenuItem>();
childMap.put(lastParent, children);
}
for (ColorMapMenuItem item : children) {
if (item.subMenu != null && item.text.equals(text)) {
found = true;
lastParent = item.subMenu;
break;
}
}
if (!found) {
Menu cmapParent = lastParent;
lastParent = new Menu(shell, SWT.DROP_DOWN);
new ColorMapMenuItem(cmapParent, lastParent, null, text);
}
}
}
}
sortMenus(cmapPopupMenu);
cmapPopupMenu.addMenuListener(new MenuPopulator(cmapPopupMenu,
ColorMapTreeFactory.getBaseTree()));
}
/**
@ -284,60 +178,6 @@ public class ColormapComp {
refreshItems();
}
/**
* @param parent
*/
private void sortMenus(Menu parent) {
List<ColorMapMenuItem> children = childMap.get(parent);
if (children == null || children.size() == 0) {
return;
}
Collections.sort(children, new Comparator<ColorMapMenuItem>() {
@Override
public int compare(ColorMapMenuItem o1, ColorMapMenuItem o2) {
// catch user menu item
if (o1.text.toUpperCase().equals("USER")) {
// user is always last
return 1;
} else if (o2.text.toUpperCase().equals("USER")) {
return -1;
}
// catch site menu item
if (o1.text.toUpperCase().equals("SITE")) {
// site is after all but user ( user gets caught first )
return 1;
} else if (o2.text.toUpperCase().equals("SITE")) {
return -1;
}
// all other menu items
if (o1.subMenu != null && o2.subMenu == null) {
// Show those with submenus first
return -1;
} else if (o1.subMenu == null && o2.subMenu != null) {
// Show those with submenus first
return 1;
} else {
// else look at text
return o1.text.toLowerCase().compareTo(
o2.text.toLowerCase());
}
}
});
for (ColorMapMenuItem child : children) {
child.initializeComponents();
}
for (ColorMapMenuItem child : children) {
if (child.subMenu != null) {
sortMenus(child.subMenu);
}
}
}
public Menu getMenu() {
return cmapPopupMenu;
}
@ -348,4 +188,160 @@ public class ColormapComp {
}
return false;
}
protected void changeColormap(String name) {
if (cmapButton != null) {
cmapButton.setText(name);
}
try {
params.setColorMap(ColorMapLoader.loadColorMap(name));
cap.notifyResources();
for (IColormapCompChangeListener listener : listeners) {
listener.colormapChanged(name);
}
} catch (VizException e) {
statusHandler.handle(Priority.ERROR, "Unable to change colormap.",
e);
}
}
/**
* Class to recursively populate a menu based off the contents of a
* {@link ColorMapTree}. The menu is not populated until it is shown. This
* is also an eclipse Job that will automatically try to prefetch some
* information from the tree for faster population.
*/
private class MenuPopulator extends Job implements MenuListener {
private final Menu menu;
private final ColorMapTree tree;
public MenuPopulator(Menu menu, ColorMapTree tree) {
super("Loading Color Maps");
this.menu = menu;
this.tree = tree;
schedule();
}
@Override
public void menuHidden(MenuEvent e) {
/* Do Nothing */
}
/**
* Get all the subTrees that will be displayed and call
* optimizeIsEmpty(). isEmpty() is usually the slowest operation in
* menuShown because it can go recursive. optimizeIsEmpty ensures that
* future calls(from menuShown) are as fast as possible.
*/
@Override
protected IStatus run(IProgressMonitor monitor) {
for (ColorMapTree subTree : tree.getSubTrees()) {
subTree.optimizeIsEmpty();
}
for (ColorMapTree subTree : getLevelTrees()) {
subTree.optimizeIsEmpty();
}
return Status.OK_STATUS;
}
/**
* Fill the menu with items from the tree. Always create menus at the
* top even if there are other items
*/
@Override
public void menuShown(MenuEvent e) {
int index = 0;
List<ColorMapTree> subTrees = tree.getSubTrees();
Collections.sort(subTrees, new Comparator<ColorMapTree>() {
@Override
public int compare(ColorMapTree tree1, ColorMapTree tree2) {
return tree1.getName().compareToIgnoreCase(tree2.getName());
}
});
for (ColorMapTree subTree : subTrees) {
if (!subTree.isEmpty()) {
addSubTree(subTree, index++);
}
}
List<LocalizationFile> files = tree.getColorMapFiles();
Collections.sort(files, new Comparator<LocalizationFile>() {
@Override
public int compare(LocalizationFile file1,
LocalizationFile file2) {
return file1.getName().compareToIgnoreCase(file2.getName());
}
});
for (LocalizationFile file : files) {
addFile(file, index++);
}
for (ColorMapTree subTree : getLevelTrees()) {
if (!subTree.isEmpty()) {
addSubTree(subTree, index++);
}
}
menu.removeMenuListener(this);
}
/**
* The root menu should show not only the base tree, but also an
* additional menu item for each localization level(SITE, USER ...),
* this method gets those trees when they are needed so menu items can
* be added.
*/
private List<ColorMapTree> getLevelTrees() {
if (menu == cmapPopupMenu) {
List<ColorMapTree> trees = new ArrayList<ColorMapTree>();
IPathManager pm = PathManagerFactory.getPathManager();
LocalizationLevel[] levels = pm.getAvailableLevels();
for (LocalizationLevel level : levels) {
if (level != LocalizationLevel.BASE) {
ColorMapTree tree = ColorMapTreeFactory
.getTreeForLevel(level);
trees.add(tree);
}
}
return trees;
} else {
return Collections.emptyList();
}
}
private void addSubTree(ColorMapTree tree, int index) {
MenuItem item = new MenuItem(menu, SWT.CASCADE, index);
item.setText(tree.getName());
Menu subMenu = new Menu(shell, SWT.DROP_DOWN);
item.setMenu(subMenu);
subMenu.addMenuListener(new MenuPopulator(subMenu, tree));
}
private void addFile(LocalizationFile file, int index) {
MenuItem item = new MenuItem(menu, SWT.None, index);
final String name = ColorMapLoader.shortenName(file);
int start = name.lastIndexOf(PathManager.SEPARATOR);
if (start >= 0) {
item.setText(name.substring(start + 1));
} else {
item.setText(name);
}
item.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
changeColormap(name);
}
});
}
}
}