blob: 7bafe15310c6b9a54b15e51e5098e2430cfc70c6 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2007 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
* Bundesinstitut für Risikobewertung
*******************************************************************************/
package org.eclipse.stem.ui.views.graphmap;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
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.emf.common.notify.Adapter;
import org.eclipse.emf.common.notify.AdapterFactory;
import org.eclipse.emf.common.util.EMap;
import org.eclipse.emf.common.util.URI;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.stem.core.common.Identifiable;
import org.eclipse.stem.core.graph.DynamicLabel;
import org.eclipse.stem.core.graph.DynamicNodeLabel;
import org.eclipse.stem.core.graph.Edge;
import org.eclipse.stem.core.graph.Node;
import org.eclipse.stem.core.model.Decorator;
import org.eclipse.stem.data.geography.centers.GeographicCenters;
import org.eclipse.stem.definitions.adapters.spatial.geo.LatLong;
import org.eclipse.stem.definitions.adapters.spatial.geo.LatLong.Segment;
import org.eclipse.stem.definitions.adapters.spatial.geo.LatLongProvider;
import org.eclipse.stem.definitions.adapters.spatial.geo.LatLongProviderAdapter;
import org.eclipse.stem.definitions.adapters.spatial.geo.LatLongProviderAdapterFactory;
import org.eclipse.stem.definitions.edges.PopulationEdge;
import org.eclipse.stem.diseasemodels.standard.DiseaseModelLabel;
import org.eclipse.stem.jobs.simulation.ISimulation;
import org.eclipse.stem.jobs.simulation.ISimulationListener;
import org.eclipse.stem.jobs.simulation.SimulationEvent;
import org.eclipse.stem.populationmodels.standard.PopulationModelLabel;
import org.eclipse.stem.ui.adapters.color.ColorProviderAdapter;
import org.eclipse.stem.ui.adapters.color.ColorProviderAdapterFactory;
import org.eclipse.stem.ui.adapters.color.IColorProviderChangedListener;
import org.eclipse.stem.ui.widgets.GeoViewOptionsBar;
import org.eclipse.stem.ui.widgets.GeoViewOptionsBar.PropertySelectionEvent;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.FormAttachment;
import org.eclipse.swt.layout.FormData;
import org.eclipse.swt.layout.FormLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
/**
* This class represents a visualization of the geographic attributes of a
* {@link org.eclipse.stem.jobs.simulation.org.eclipse.stem.jobs.simulation.Simulation}
* .
*/
public class GraphMapControl extends Composite implements ISimulationListener,
ISelectionProvider, IColorProviderChangedListener,
ISelectionChangedListener, GeoViewOptionsBar.PropertySelectionListener {
private ISimulation simulation;
private ISelection selection;
private List<ISelectionChangedListener> listeners;
private GraphMapRenderer geographicRenderer;
private String selectedEdge;
private Job refreshJob;
private boolean refreshPending;
private StemPolygonList polygonsToDraw;
private ColorProviderAdapter colorProviderAdapter;
private Label simulationNameLabel;
private GeoViewOptionsBar optionsBar;
private Map<Decorator, DecoratorDisplayData> decoratorToDecoratorDisplayDataMap;
private Map<LatLong, StemPolygonList> polygonListMap;
public GraphMapControl(Composite parent, int style) {
super(parent, style);
listeners = new CopyOnWriteArrayList<ISelectionChangedListener>();
decoratorToDecoratorDisplayDataMap = new HashMap<Decorator, DecoratorDisplayData>();
polygonListMap = new HashMap<LatLong, StemPolygonList>();
geographicRenderer = new GraphMapRenderer(this, SWT.NONE);
geographicRenderer.setColorProviderAdapter(colorProviderAdapter);
geographicRenderer.addSelectionChangedListener(this);
setLayout(new FormLayout());
Composite topComposite = new Composite(this, SWT.NONE);
topComposite.setLayout(new FillLayout(SWT.HORIZONTAL));
simulationNameLabel = new Label(topComposite, SWT.LEFT);
simulationNameLabel.setText("");
geographicRenderer.setGainScaleLabel(new Label(topComposite, SWT.RIGHT));
geographicRenderer.updateScaleLogLinLabel();
optionsBar = new GeoViewOptionsBar(this, SWT.NONE);
optionsBar.addColorProviderChangedListener(this);
optionsBar.addPropertySelectionListener(this);
FormData topCompositeFormData = new FormData();
topComposite.setLayoutData(topCompositeFormData);
topCompositeFormData.top = new FormAttachment(0, 0);
topCompositeFormData.left = new FormAttachment(0, 0);
topCompositeFormData.right = new FormAttachment(100, 0);
FormData geoRendererFormData = new FormData();
geographicRenderer.setLayoutData(geoRendererFormData);
geoRendererFormData.top = new FormAttachment(topComposite, 0);
geoRendererFormData.bottom = new FormAttachment(optionsBar, 0);
geoRendererFormData.left = new FormAttachment(0, 0);
geoRendererFormData.right = new FormAttachment(100, 0);
FormData optionsBarFormData = new FormData();
optionsBar.setLayoutData(optionsBarFormData);
optionsBarFormData.bottom = new FormAttachment(100, 0);
optionsBarFormData.left = new FormAttachment(0, 0);
optionsBarFormData.right = new FormAttachment(100, 0);
pack();
}
public ISimulation getSimulation() {
return simulation;
}
/**
* Setting the {@link org.eclipse.stem.jobs.simulation.Simulation} has the
* side-effect of causing the control to remove itself as a listener from
* any previously set {@link org.eclipse.stem.jobs.simulation.Simulation}
* and adding itself as a listener to the new one. It will also cause the
* image to be initialized from the contents of the new
* {@link org.eclipse.stem.jobs.simulation.Simulation} as appropriate.
*
* @param simulation
* the {@link org.eclipse.stem.jobs.simulation.Simulation} whose
* Lat/Long data will be rendered.
*/
public void setSimulation(ISimulation simulation) {
// Any need to switch?
if (this.simulation == simulation) {
// No
return;
} // if no need to switch?
// Should we remove ourselves as listeners from the current simulation?
if (this.simulation != null) {
// Yes
this.simulation.removeSimulationListener(this);
}
this.simulation = simulation;
selection = null;
// Was there a new simulation to switch to?
if (this.simulation != null) {
// Yes
this.simulation.addSimulationListener(this);
simulationNameLabel.setText(this.simulation.getName());
optionsBar.setSimulation(this.simulation);
}
refresh();
}
/**
* Update the contents of {@link #geographicRenderer} from the contents of
* the {@link #simulation}. This method should only be called by the UI
* thread otherwise it contains a race condition that can lock up the
* refreshing of the map canvas.
*/
public void refresh() {
// This is complex and potential confusing code. The basic idea is that
// we want to determine what to draw on the mapCanvas in a thread
// separate from the UI thread, but we also want to capture a request to
// refresh the mapCanvas that comes while a background Job is already
// running. This request could have been generated because something
// significant like a decorator or a property has changed and
// we don't want to lose that switch.
// We have two fields that capture the state of the refresh. The first
// is the flag refreshPending which if true indicates that a call to
// refreshMapCanvas was processed while a background Job was executing.
// This flag is set instead of creating another background Job. The
// second is the field refreshJob which is set to the background Job if
// it is present and running.
//
// If we enter the method and refreshPending is true then we immediately
// exit because a refresh job will be created and there's nothing more
// for us to request. If, instead, refreshPending is false, then we
// check to see if a refreshJob is already running. If so, we set
// refreshPending to true, and exit. If not, we create the background
// refresh job and assign it to the field refreshJob.
//
// In the background refreshing job we process the Lat/Long data and
// then when done we add a Runnable on to the UI thread to redraw the
// mapCanvas. That Runnable also checks to see if the refresh pending
// flag is set and if so, resets the flag and then adds another Runnable
// to the UI thread to re-enter the refreshMapCanvas method.
//
// There is a race condition in the method if called by other than the
// UI thread. If a thread different from the UI thread were to call this
// method then it could reach position "RC #2" in the code below and
// then be preempted. The UI thread could then execute the test at "RC
// #1" below and find that there is no refresh pending. It would exit
// and then the other thread would resume at "RC #2" and set the
// refreshPending flag. This would cause all subsequent method
// invocations to immediately exit and the map canvas would never be
// refreshed again.
if (refreshJob != null) {
refreshPending = true;
return;
}
refreshJob = new Job(Messages.getString("IMView.MRefresh")) {
@Override
protected IStatus run(IProgressMonitor monitor) {
polygonsToDraw = createPolygonsToDraw(monitor);
if (polygonsToDraw == null) {
return Status.OK_STATUS;
}
try {
Display display = Display.getDefault();
if (!display.isDisposed()) {
display.asyncExec(new Runnable() {
@Override
public void run() {
if (!geographicRenderer.isDisposed()) {
geographicRenderer.render(polygonsToDraw,
simulation);
refreshJob = null;
if (refreshPending) {
refreshPending = false;
refresh();
}
} else {
refreshJob = null;
}
}
});
}
} catch (NullPointerException npe) {
// See 177966:
// We ignore the exception, there's nothing to do
}
monitor.done();
return Status.OK_STATUS;
}
};
refreshJob.schedule();
}
/**
* The method will return the {@link GeographicRenderer} used by this
* object.
*
* @return The GeographicRenderer used by this object
* @see GeographicRenderer
*/
public GraphMapRenderer getGeographicRenderer() {
return geographicRenderer;
}
/**
* Pass a {@link SelectionChangedEvent} along to listeners.
*
* @param selection
* the {@link ISelection} to be giving to the listeners.
*/
public void fireSelection(ISelection selection) {
SelectionChangedEvent event = new SelectionChangedEvent(this, selection);
for (ISelectionChangedListener listener : listeners) {
listener.selectionChanged(event);
}
}
@Override
public void simulationChanged(SimulationEvent event) {
switch (event.getSimulationState()) {
case COMPLETED_CYCLE:
// It could be that the propertySelector is not initialized
// yet. This could happen if, when the simulation is first set, its
// scenario was not completely initialized, and so didn't have a
// canonical graph. If this is the case, we need to try again.
if (!optionsBar.isInitialized()) {
optionsBar.setSimulation(getSimulation());
}
refresh();
break;
case RESET:
refresh();
break;
}
}
@Override
public void colorProviderChanged(Class selectedColorProvider) {
Decorator decorator = optionsBar.getSelectedDecorator();
if (decorator == null) {
return;
}
AdapterFactory adapterFactory = ColorProviderAdapterFactory.INSTANCE
.getFactoryForType(selectedColorProvider);
Adapter adapter = adapterFactory.adapt(decorator.getGraph(),
selectedColorProvider);
if (adapter instanceof ColorProviderAdapter) {
colorProviderAdapter = (ColorProviderAdapter) adapter;
colorProviderAdapter.setSelectedDecorator(optionsBar
.getSelectedDecorator());
colorProviderAdapter.setSelectedPopulationIdentifier(optionsBar
.getSelectedPopulationIdentifier());
geographicRenderer.setColorProviderAdapter(colorProviderAdapter);
layout();
}
}
@Override
public void dispose() {
super.dispose();
optionsBar.removeColorProviderChangedListener(this);
optionsBar.removePropertySelectionListener(this);
geographicRenderer.removeSelectionChangedListener(this);
if (simulation != null) {
simulation.removeSimulationListener(this);
}
// Shouldn't be any selection changed listeners
assert listeners.size() == 0;
}
@Override
public void selectionChanged(SelectionChangedEvent event) {
fireSelection(event.getSelection());
}
@Override
public void addSelectionChangedListener(ISelectionChangedListener listener) {
listeners.add(listener);
}
@Override
public void removeSelectionChangedListener(
ISelectionChangedListener listener) {
listeners.remove(listener);
}
@Override
public ISelection getSelection() {
return selection;
}
@Override
public void setSelection(ISelection selection) {
this.selection = selection;
fireSelection(selection);
}
@Override
public void propertySelected(PropertySelectionEvent propertySelectionEvent) {
if (propertySelectionEvent == null) {
selectedEdge = null;
} else if (propertySelectionEvent.getSource() instanceof String) {
selectedEdge = (String) propertySelectionEvent.getSource();
}
refresh();
}
/**
* This runs in the background {@link Job} created in {@link #refresh()}
*
* @param decorator
* the {@link Decorator} for whom that data is being retrieved.
* @param monitor
* the progress monitor to use to report the progress of
* obtaining the data
* @return the {@link DecoratorDisplayData} for the {@link Decorator}.
*/
private DecoratorDisplayData getDecoratorDisplayData(Decorator decorator,
IProgressMonitor monitor) {
DecoratorDisplayData retValue = decoratorToDecoratorDisplayDataMap
.get(decorator);
// Is there already an instance for this decorator?
if (retValue == null) {
retValue = new DecoratorDisplayData();
decoratorToDecoratorDisplayDataMap.put(decorator, retValue);
if (decorator.getLabelsToUpdate() == null
|| decorator.getLabelsToUpdate().isEmpty()) {
return retValue;
}
DynamicNodeLabel firstNodeLabel = (DynamicNodeLabel) decorator
.getLabelsToUpdate().get(0);
if (firstNodeLabel != null) {
Node node = firstNodeLabel.getNode();
LatLongProviderAdapter latLongProvider = (LatLongProviderAdapter) LatLongProviderAdapterFactory.INSTANCE
.adapt(node, LatLongProvider.class);
retValue.setLatLongProviderAdapter(latLongProvider);
}
for (DynamicLabel label : decorator.getLabelsToUpdate()) {
retValue.addLabel((DynamicNodeLabel) label);
}
}
// Does the instance have all of it's lat/long data retrieved yet?
if (!retValue.hasAllLatLong()) {
monitor.subTask(decorator.getDublinCore().getTitle());
retValue.updateLatLong(monitor);
}
return retValue;
}
/**
* This runs in the background {@link Job} created in
* {@link #refreshMapCanvas()}.
*
* @param monitor
* progress monitor for obtaining the polygons
* @return a collection of polygon values to render in the
* {@link #mapCanvas}
*/
private StemPolygonList createPolygonsToDraw(IProgressMonitor monitor) {
Decorator selectedDecorator = optionsBar.getSelectedDecorator();
String selectedPopulationIdentifier = optionsBar
.getSelectedPopulationIdentifier();
StemPolygonList retValue = new StemPolygonList();
if (selectedDecorator == null) {
return null;
}
DecoratorDisplayData displayData = getDecoratorDisplayData(
selectedDecorator, monitor);
if (displayData == null) {
return null;
}
monitor.beginTask(selectedDecorator.getDublinCore().getTitle(),
displayData.getLabelData().size());
for (Map.Entry<DynamicNodeLabel, LatLong> labelData : displayData
.getLabelData().entrySet()) {
DynamicNodeLabel lab = labelData.getKey();
// Check that the population identifier matches. If not, skip it
if (lab instanceof DiseaseModelLabel
&& !((DiseaseModelLabel) lab).getPopulationModelLabel()
.getPopulationIdentifier()
.equals(selectedPopulationIdentifier)) {
continue;
} else if (lab instanceof PopulationModelLabel
&& !((PopulationModelLabel) lab).getPopulationIdentifier()
.equals(selectedPopulationIdentifier)) {
continue;
}
// This is the Identifiable that will produce the lat/long data.
Identifiable identifiable = labelData.getKey().getIdentifiable();
LatLong latLong = labelData.getValue();
monitor.subTask(identifiable.getDublinCore().getTitle());
if (latLong != null && latLong.size() > 0) {
StemPolygonList stemPolygonsList = getStemPolygonsList(latLong,
identifiable);
retValue.addAll(stemPolygonsList);
}
monitor.worked(1);
}
if (selectedEdge == null) {
return retValue;
}
EMap<URI, Edge> allEdges = selectedDecorator.getGraph().getEdges();
List<Edge> edges = new ArrayList<Edge>();
for (URI uri : allEdges.keySet()) {
if (uri.toString().startsWith(selectedEdge)) {
Edge edge = allEdges.get(uri);
if (edge instanceof PopulationEdge) {
String popID = ((PopulationEdge) edge)
.getPopulationIdentifier();
if (selectedPopulationIdentifier.equals(popID)) {
edges.add(edge);
}
} else {
edges.add(edge);
}
}
}
if (edges.isEmpty()) {
return retValue;
}
Set<URI> addedEdges = new HashSet<URI>();
for (Edge nextEdge : edges) {
Node nodeA = nextEdge.getA();
Node nodeB = nextEdge.getB();
if (nodeA == null || nodeB == null) {
// It is possible that edges for air transport has a missing
// node if sub model is not included in the scenario.
continue;
}
String nodeAISOKey = nodeA.getURI().lastSegment();
String nodeBISOKey = nodeB.getURI().lastSegment();
double[] centerA = GeographicCenters.getCenter(nodeAISOKey);
double[] centerB = GeographicCenters.getCenter(nodeBISOKey);
if (centerA == null) {
LatLongProviderAdapter latLongProviderA = (LatLongProviderAdapter) LatLongProviderAdapterFactory.INSTANCE
.adapt(nodeA, LatLongProvider.class);
latLongProviderA.setTarget(nodeA);
centerA = latLongProviderA.getCenter();
}
if (centerB == null) {
LatLongProviderAdapter latLongProviderB = (LatLongProviderAdapter) LatLongProviderAdapterFactory.INSTANCE
.adapt(nodeB, LatLongProvider.class);
latLongProviderB.setTarget(nodeB);
centerB = latLongProviderB.getCenter();
}
if (centerA == null || centerB == null) {
continue;
}
if (!addedEdges.contains(nextEdge.getURI())) {
Segment edgeSegment = new Segment(new double[][] { centerA,
centerB });
StemPolygon stemPolygon = new StemPolygon(edgeSegment, nextEdge);
retValue.add(stemPolygon);
addedEdges.add(nextEdge.getURI());
}
}
return retValue;
}
private StemPolygonList getStemPolygonsList(LatLong latLong,
Identifiable identifiable) {
StemPolygonList retValue = polygonListMap.get(latLong);
if (retValue == null) {
retValue = new StemPolygonList(latLong, identifiable);
polygonListMap.put(latLong, retValue);
}
return retValue;
}
}