blob: ebdf6c01716383fcd87703017aacd0350460c910 [file] [log] [blame]
// MapCanvas.java
package org.eclipse.stem.ui.views.geographic.map;
/*******************************************************************************
* 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
*******************************************************************************/
import java.io.File;
import java.io.IOException;
import java.util.ConcurrentModificationException;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.window.DefaultToolTip;
import org.eclipse.stem.core.common.DublinCore;
import org.eclipse.stem.core.common.Identifiable;
import org.eclipse.stem.core.graph.Edge;
import org.eclipse.stem.core.graph.Node;
import org.eclipse.stem.core.graph.NodeLabel;
import org.eclipse.stem.core.model.STEMTime;
import org.eclipse.stem.core.sequencer.Sequencer;
import org.eclipse.stem.data.geography.GeographicNames;
import org.eclipse.stem.definitions.labels.AreaLabel;
import org.eclipse.stem.definitions.labels.PopulationLabel;
import org.eclipse.stem.definitions.nodes.Region;
import org.eclipse.stem.diseasemodels.standard.StandardDiseaseModelLabel;
import org.eclipse.stem.jobs.simulation.ISimulation;
import org.eclipse.stem.populationmodels.standard.PopulationModelLabel;
import org.eclipse.stem.ui.adapters.color.ColorProviderAdapter;
import org.eclipse.stem.ui.adapters.color.ColorScaleComposite;
import org.eclipse.stem.ui.adapters.color.StandardColorProvider;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.MouseTrackAdapter;
import org.eclipse.swt.events.MouseWheelListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.ImageLoader;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
/**
* This class is a SWT Widget that displays a "map" view of the geographic
* features of a {@link org.eclipse.stem.jobs.simulation.Simulation}.
*/
public class MapCanvas
extends Canvas
implements ISelectionProvider, PaintListener, MouseWheelListener, MouseMoveListener, DisposeListener
{
private static final int TOOLTIP_HIDE_DELAY = 30000; // milliseconds
private static final double INITIAL_ZOOM_FACTOR = 1;
private static final double INITIAL_X_TRANSLATION = 0;
private static final double INITIAL_Y_TRANSLATION = 0;
protected static final double ZOOMING_FACTOR = 1.1;
protected static final double UNZOOMING_FACTOR = 1 / ZOOMING_FACTOR;
protected static final int BORDER_ALPHA = 25;
/**
* This is the list of polygons that are rendered.
*/
private StemPolygonsList polygonsToRender;
double zoomFactor = INITIAL_ZOOM_FACTOR;
private float gainFactor = 1.0f;
private double xTranslation = INITIAL_X_TRANSLATION;
private double yTranslation = INITIAL_Y_TRANSLATION;
boolean drawPolygonBorders = true;
StemPolygonTransform pointsTransformer = new StemPolygonTransform();
boolean toUpdateTranform = true;
private final StandardColorProvider stdColorProvider = new StandardColorProvider(this.getDisplay());
private ColorProviderAdapter colorProvider = null;
private boolean useLogScaling = true;
boolean leftMouseButtonPressed = false;
int lastOffsetX;
int lastOffsetY;
ColorScaleComposite colorScale = null;
private ISelection selection;
/**
* The collection of ISelectionChangedListener waiting to be told about
* selections.
*/
protected final List<ISelectionChangedListener> selectionChangedListeners = new CopyOnWriteArrayList<ISelectionChangedListener>();
private Rectangle polygonsBoundsRect = null;
private static final int MARGIN = 10;
private final MouseTrackHandler mouseTrackHandler = new MouseTrackHandler();
private final MouseButtonHandler mouseButtonHandler = new MouseButtonHandler();
private final MouseHoverHandler mouseHoverHandler = new MouseHoverHandler();
private final KeyHandler keyHandler = new KeyHandler();
protected Image canvasImage;
private ISimulation simulation;
private STEMTime time;
private Sequencer sequencer;
private DefaultToolTip tooltip;
/**
* @param parent
* @param style
*/
public MapCanvas(final Composite parent, final int style) {
super(parent, SWT.DOUBLE_BUFFERED | SWT.NO_BACKGROUND);
addPaintListener(this);
addMouseWheelListener(this);
addMouseTrackListener(mouseTrackHandler);
addMouseMoveListener(this);
addKeyListener(keyHandler);
addDisposeListener(this);
addMouseListener(mouseButtonHandler);
addMouseTrackListener(mouseHoverHandler);
tooltip = new DefaultToolTip(this);
tooltip.setHideDelay(TOOLTIP_HIDE_DELAY);
} // MapCanvas
/**
* @see org.eclipse.swt.events.PaintListener#paintControl(org.eclipse.swt.events.PaintEvent)
*/
public void paintControl(final PaintEvent e) {
final GC gc = e.gc;
final Point controlSize = ((Control) e.getSource()).getSize();
toUpdateTranform = true;
Image newCanvasImage = new Image(e.display, controlSize.x, controlSize.y);
GC localGC = new GC(newCanvasImage);
draw(localGC, 0, 0, controlSize.x, controlSize.y);
localGC.dispose();
gc.drawImage(newCanvasImage, 0, 0);
Image oldCanvasImage = canvasImage;
canvasImage = newCanvasImage;
if (oldCanvasImage != null && !oldCanvasImage.isDisposed()) {
oldCanvasImage.dispose();
}
}
/**
* @param path
* @throws IOException
*/
public void saveCanvasImage(File path) throws IOException
{
try {
if (canvasImage != null && !canvasImage.isDisposed()) {
ImageLoader loader = new ImageLoader();
loader.data = new ImageData[] {canvasImage.getImageData()};
loader.save(path.getAbsolutePath(), SWT.IMAGE_PNG);
}
} catch (Exception e) {
throw new IOException("MapCanvas:saveCanvasImage() Unable to save map canvas image to the file "+ path.getAbsolutePath());
}
}
/**
* @return
*/
public Image getCanvasImage()
{
return canvasImage;
}
/**
* @param e
*/
public void mouseScrolled(final MouseEvent e) {
// Zoom Out?
if (e.count >= 0) {
// Yes
zoomIn();
} else {
zoomOut();
}
}
/**
* @param e
*/
public void mouseMove(MouseEvent e) {
if (leftMouseButtonPressed) {
// Yes
// new x and y are defined by current mouse location subtracted
// by previously processed mouse location
final int newX = e.x - lastOffsetX;
final int newY = e.y - lastOffsetY;
lastOffsetX = e.x;
lastOffsetY = e.y;
// Did we move from the spot where the mouse went down?
if (newX != 0 || newY != 0) {
// Yes
// This is a translation of the map then
// update the canvas translation
addTranslation(newX, newY);
} // if moved
}
}
/**
* @see org.eclipse.swt.events.DisposeListener#widgetDisposed(org.eclipse.swt.events.DisposeEvent)
*/
public void widgetDisposed(@SuppressWarnings("unused") final DisposeEvent e) {
if (!isDisposed()) {
dispose();
}
}
/**
* @param polygonsToRender
*/
public void render(final StemPolygonsList polygonsToRender, ISimulation sim) {
simulation = sim;
this.polygonsToRender = polygonsToRender;
redraw();
} // render
/**
* @param drawPolygonBorders
*/
public final void setDrawPolygonBorders(final boolean drawPolygonBorders) {
this.drawPolygonBorders = drawPolygonBorders;
}
/**
* @param gainFactor
*/
public final void setGainFactor(final float gainFactor) {
this.gainFactor = gainFactor;
}
/**
* @param useLogScaling
*/
public final void setUseLogScaling(final boolean useLogScaling) {
this.useLogScaling = useLogScaling;
}
/**
* Reset
*/
public void reset() {
xTranslation = INITIAL_X_TRANSLATION;
yTranslation = INITIAL_Y_TRANSLATION;
zoomFactor = INITIAL_ZOOM_FACTOR;
} // reset
/**
* The method which gets the MapCanvas' polygons list, and draws it on the
* MapCanvas.
*
* @param gc
* @param x
* @param y
* @param width
* @param height
*/
void draw(final GC gc, final int x, final int y, final int width,
final int height) {
gc.setBackground(stdColorProvider.getBackgroundColor());
drawBackground(gc, x, y, width, height);
gc.setLineWidth(0); //Zero means the fastest possible line drawing algorithm
if (polygonsToRender == null || polygonsToRender.isEmpty()) {
return;
}
//Do we need to recalculate the transform?
if (toUpdateTranform) {
//Yes, compute it
computeTransform(polygonsToRender);
//Update all polygons about the changes in the transform
for (StemPolygon stempoly : polygonsToRender) {
stempoly.setPointsTransformer(pointsTransformer);
}
toUpdateTranform = false;
}
Color bordersColor = stdColorProvider.getBordersColor();
for (final StemPolygon stempoly : polygonsToRender) {
Identifiable identifiable = stempoly.getIdentifiable();
if (identifiable instanceof Node) {
colorProvider.setTarget(identifiable);
//Update the G2D with the appropriate color before filling the polygon
colorProvider.updateGC(gc, gainFactor, useLogScaling);
gc.fillPolygon(stempoly.transformedPoints);
if (drawPolygonBorders) {
gc.setForeground(bordersColor);
gc.setAlpha(BORDER_ALPHA);
gc.drawPolygon(stempoly.transformedPoints);
gc.setAlpha(255); // set it back
}
}
if (identifiable instanceof Edge) {
gc.setForeground(stdColorProvider.getEdgesColor());
if (stempoly.transformedPoints == null) {
stempoly.setPointsTransformer(pointsTransformer);
}
gc.drawLine(
stempoly.transformedPoints[0],
stempoly.transformedPoints[1],
stempoly.transformedPoints[2],
stempoly.transformedPoints[3]);
}
} // for each StemPolygon
// if(colorProvider instanceof AbstractRelativeValueColorProviderAdapter) {
// AbstractRelativeValueColorProviderAdapter c = (AbstractRelativeValueColorProviderAdapter) colorProvider;
// drawColorScale(c);
// }
gc.setForeground(getDisplay().getSystemColor(SWT.COLOR_BLACK));
} // draw
/**
*
* @param adapter
*/
// private void drawColorScale(AbstractRelativeValueColorProviderAdapter adapter) {
// Color saturatedColor = adapter.getForegroundFillColor(); // to transparent color over the background
// Color textColor = saturatedColor; // text
// Color backGround = adapter.getBackgroundFillColor(); // textBackground
//
// if (colorScale == null) {
// colorScale = new ColorScaleComposite(this, saturatedColor, new double[]{0.0, 1.0/gainFactor}, useLogScaling, SWT.BORDER, backGround, textColor);
// }
// colorScale.updateColorScale(saturatedColor, new double[]{0.0, 1.0/gainFactor},useLogScaling, backGround, textColor);
// }
/**
* @param polygonList
*/
private void computeTransform(final StemPolygonsList polygonList) {
if (polygonsBoundsRect == null) {
polygonsBoundsRect = polygonList.getBounds();
if (polygonsBoundsRect == null) {
return;
}
}
final Rectangle canvasBounds = getBounds();
final int effectiveCanvasWidth = canvasBounds.width - 2 * MARGIN;
final int effectiveCanvasHeight = canvasBounds.height - 2 * MARGIN;
final double width = polygonsBoundsRect.width;
final double height = polygonsBoundsRect.height;
final double WIDTH_RATIO = effectiveCanvasWidth / width;
final double HEIGHT_RATIO = effectiveCanvasHeight / height;
final double SCALE_FACTOR = Math.min(WIDTH_RATIO, HEIGHT_RATIO)
* zoomFactor;
// Figure out the extra translation needed to center the image either
// vertically or horizontally in the canvas
int xCenteringTranslation = 0;
int yCenteringTranslation = 0;
// Anything to center?
if (width > 0 && height > 0) {
// Yes
xCenteringTranslation = (effectiveCanvasWidth - (int) (width * SCALE_FACTOR)) / 2;
yCenteringTranslation = (effectiveCanvasHeight - (int) (height * SCALE_FACTOR)) / 2;
} // if anything to center
int boundsMinX = polygonsBoundsRect.x;
int boundsMinY = polygonsBoundsRect.y;
pointsTransformer.setOffsetX(-boundsMinX * SCALE_FACTOR + MARGIN + xCenteringTranslation + xTranslation * zoomFactor);
pointsTransformer.setOffsetY(-boundsMinY * SCALE_FACTOR + MARGIN + yCenteringTranslation + yTranslation * zoomFactor);
pointsTransformer.setScale(SCALE_FACTOR);
} // computeTransform
void zoomIn() {
zoomFactor *= ZOOMING_FACTOR;
toUpdateTranform = true;
redraw();
}
void zoomOut() {
zoomFactor *= UNZOOMING_FACTOR;
toUpdateTranform = true;
redraw();
}
/**
*
*/
protected static class MouseTrackHandler extends MouseTrackAdapter {
@Override
public void mouseEnter(@SuppressWarnings("unused")
final MouseEvent e) {
// The Map does not NEED to steal focus at all
// IF in the future we need focus then the following
// Guard claused protects against irrelevant/unneeded stealing of focus
// if there is NOTHING in the map, don't steal focus
//if (polygonsToRender != null && !polygonsToRender.isEmpty()) {
//forceFocus();
//}
}
}
/**
*
*/
protected class KeyHandler extends KeyAdapter {
@Override
public void keyReleased(final KeyEvent e) {
switch (e.keyCode) {
case SWT.ARROW_UP:
zoomIn();
break;
case SWT.ARROW_DOWN:
zoomOut();
break;
default:
break;
} // switch
}
}
/**
* A helper class that defines a mouse-listener that will provide the user
* the ability to move the map inside the view.
*
*/
protected class MouseButtonHandler extends MouseAdapter {
/**
* @see org.eclipse.swt.events.MouseListener#mouseDown(org.eclipse.swt.events.MouseEvent)
*/
@Override
public void mouseDown(final MouseEvent e) {
// Is the left mouse button?
if (e.button == 1) {
// Yes
// Capture the starting point
lastOffsetX = e.x;
lastOffsetY = e.y;
leftMouseButtonPressed = true;
} // if
} // mouseDown
/**
* @see org.eclipse.swt.events.MouseListener#mouseUp(org.eclipse.swt.events.MouseEvent)
*/
@Override
public void mouseUp(final MouseEvent e) {
// Is it the left mouse button?
if (e.button == 1) {
// Yes
// new x and y are defined by current mouse location subtracted
// by previously processed mouse location
final int newX = e.x - lastOffsetX;
final int newY = e.y - lastOffsetY;
// Did we move from the spot where the mouse went down?
if (newX != 0 || newY != 0) {
// Yes
// This is a translation of the map then
// update the canvas translation
addTranslation(newX, newY);
} // if moved
else {
// No
// This is a potential selection of a polygon
final StemPolygon polygon = getPolygon(e);
// Are we clicking on a polygon?
if (polygon != null) {
// Yes
// Get the Identifiable associated with the polygon and
// make it the current selection.
// Build the GeographicSelectionElements
// and pass it via the StructuredSelection as the 2nd
// element
final GeographicSelectionElements gse = new GeographicSelectionElements();
// Convert from canvas space coordinates to lat/long by
// the inverse
// transform.
final Point latLongPosition = pointsTransformer.getInversedPoint(e.x, e.y);//inverseMap(new Point(e.x, e.y));
final double longitude = polygon
.unScaleLongitude(latLongPosition.x);
final double latitude = polygon
.unScaleLatitude(latLongPosition.y);
gse.setPoint(longitude, latitude);
final Identifiable regnImpl = polygon.getIdentifiable();
final Object[] elements = new Object[] { regnImpl, gse };
final IStructuredSelection selection = new StructuredSelection(
elements);
fireSelection(selection);
}
} // else didn't move
leftMouseButtonPressed = false;
} // if left mouse button
//Remember the map canvas that has been clicked on
SelectedReportsManager.getInstance().setRecentClickedMapCanvas((MapCanvas)e.getSource());
} // mouseUp
} // MouseButtonHandler
protected class MouseHoverHandler extends MouseTrackAdapter {
// @Override
// public void mouseEnter(MouseEvent e) {
// final Cursor cursor = new Cursor(getDisplay(), SWT.CURSOR_SIZEALL);
// Canvas mapCanvas = (Canvas)e.getSource();
// mapCanvas.setCursor(cursor);
// super.mouseEnter(e);
// }
//
// @Override
// public void mouseExit(MouseEvent e) {
// final Cursor cursor = new Cursor(getDisplay(), SWT.CURSOR_ARROW);
// Canvas mapCanvas = (Canvas)e.getSource();
// mapCanvas.setCursor(cursor);
// super.mouseExit(e);
// }
/**
* @see org.eclipse.swt.events.MouseTrackListener#mouseHover(org.eclipse.swt.events.MouseEvent)
*/
@Override
public void mouseHover(final MouseEvent e) {
// Convert from canvas space coordinates to lat/long by the inverse
// transform.
final Point latLongPosition = pointsTransformer.getInversedPoint(e.x, e.y);//inverseMap(new Point(e.x, e.y));
// Try to get the polygon that matches the position of the mouse
final StemPolygon polygon = getPolygon(latLongPosition);
// Did we find an enclosing polygon?
if (polygon != null) {
// Yes
// We want to get the ISO-key for the identifiable, it will be
// at the end of the value of the dublin core "identifier"
// attribute
Identifiable identifiable = polygon.getIdentifiable();
final DublinCore dc = identifiable.getDublinCore();
final String dcIdentifier = dc.getIdentifier();
final String isoKey = dcIdentifier.substring(dcIdentifier
.lastIndexOf("/") + 1);
// Did we get it?
String toolTipText = null;
if (isoKey != null && !isoKey.equals("")) {
// Yes
final String geographicName = GeographicNames
.getName(isoKey);
boolean retry = true;
StringBuilder sb=null;
while(retry) {
retry = false;
try {
sb = new StringBuilder(geographicName);
sb.append(" (");
sb.append(isoKey);
sb.append(")");
Region region = (Region)identifiable;
for (NodeLabel nextLabel : region.getLabels()) {
if (nextLabel instanceof PopulationModelLabel) {
sb.append("\nPopulation (now): " + nextLabel);
}
if(nextLabel instanceof PopulationLabel) {
sb.append("\nPopulation ("+((PopulationLabel)nextLabel).getValidYear()+"):"+nextLabel);
}
if (nextLabel instanceof AreaLabel) {
sb.append("\nArea: " + nextLabel);
}
if (nextLabel instanceof StandardDiseaseModelLabel) {
sb.append("\n" + ((StandardDiseaseModelLabel)nextLabel).toTooltipString());
}
}
if (latLongPosition != null) {
double latitude = -1 * StemPolygon.getUnscaledLatitude(latLongPosition.y);
double longitude = -1 * StemPolygon.getUnscaledLongitude(latLongPosition.x);
sb.append("\nLatitude: " + latitude + ", Longitude: " + longitude);
}
sequencer = simulation.getScenario().getSequencer();
time = sequencer.getCurrentTime();
sb.append("\nTime: "+ time.toString());
} catch(ConcurrentModificationException cme) {
retry = true; // Should be rare, just retry
}
}
toolTipText = sb.toString();
} // if
else {
toolTipText = polygon.getTitle();
}
if (tooltip != null) {
if (toolTipText != null) {
tooltip.setText(toolTipText);
tooltip.show(new Point(e.x, e.y));
} else {
tooltip.hide();
}
}
/*
* // Yes //mapCanvas.setToolTipText(polygon.getTitle() + "\n"
* +polygon.getRelativeValue());
*
* String key =
* propertySelectionControl.getSelectedPropertyString();
* mapCanvas.setToolTipText("Name: " + polygon.getTitle() + "\n" +
* "Relative Value: " + polygon.getRelativeValue(key));
*/
} // if
else {
// No
if (tooltip != null) {
tooltip.hide();
}
}
} // mouseHover
} // MouseHoverHandler
/**
* @param e
* a mouse event
* @return the polygon the matches the position of the mouse, or
* <code>null</code> if there is no such polygon.
*/
StemPolygon getPolygon(final MouseEvent e) {
// Convert from canvas space coordinates to lat/long by the inverse
// transform.
//final Point latLongPosition = inverseMap(new Point(e.x, e.y));
final Point latLongPosition = pointsTransformer.getInversedPoint(e.x, e.y);
return getPolygon(latLongPosition);
} // getPolygon
/**
* @param point a point (probably within the polygon)
* @return the polygon the matches the position of the point, or
* <code>null</code> if there is no such polygon.
*/
StemPolygon getPolygon(final Point point) {
StemPolygon retValue = null;
if (point != null && polygonsToRender != null) {
// Find the Polygon that contains the lat/long coordinates
retValue = polygonsToRender.getContainingRegionPolygon(point);
} // if
return retValue;
} // getPolygon
/**
* Adds offsets to the current translation.
*
* @param xTranslationAddition
* @param yTranslationAddition
*/
protected void addTranslation(final double xTranslationAddition,
final double yTranslationAddition) {
this.xTranslation += (xTranslationAddition / zoomFactor);
this.yTranslation += (yTranslationAddition / zoomFactor);
toUpdateTranform = true;
redraw();
} // addTranslation
/**
* @see org.eclipse.jface.viewers.ISelectionProvider#addSelectionChangedListener(org.eclipse.jface.viewers.ISelectionChangedListener)
*/
public void addSelectionChangedListener(
final ISelectionChangedListener listener) {
selectionChangedListeners.add(listener);
}
/**
* @see org.eclipse.jface.viewers.ISelectionProvider#removeSelectionChangedListener(org.eclipse.jface.viewers.ISelectionChangedListener)
*/
public void removeSelectionChangedListener(
final ISelectionChangedListener listener) {
selectionChangedListeners.remove(listener);
}
/**
* @return the selection
*/
public final ISelection getSelection() {
return selection;
}
/**
* @see org.eclipse.jface.viewers.ISelectionProvider#setSelection(org.eclipse.jface.viewers.ISelection)
*/
public void setSelection(final ISelection selection) {
this.selection = selection;
fireSelection(selection);
}
void fireSelection(final ISelection selection) {
final SelectionChangedEvent event = new SelectionChangedEvent(this,
selection);
for (final ISelectionChangedListener listener : selectionChangedListeners) {
listener.selectionChanged(event);
} // for each ISelectionChangedListener
} // fireSelection
/**
* @param colorProvider the colorProvider to set
*/
public void setColorProvider(ColorProviderAdapter colorProvider) {
this.colorProvider = colorProvider;
// if(colorProvider instanceof AbstractRelativeValueColorProviderAdapter) {
// AbstractRelativeValueColorProviderAdapter c = (AbstractRelativeValueColorProviderAdapter) colorProvider;
// drawColorScale(c);
// }
}
/**
* @see org.eclipse.swt.widgets.Widget#dispose()
*/
@Override
public void dispose() {
tooltip = null;
removePaintListener(this);
removeMouseWheelListener(this);
removeMouseTrackListener(mouseTrackHandler);
removeMouseMoveListener(this);
removeKeyListener(keyHandler);
removeDisposeListener(this);
removeMouseListener(mouseButtonHandler);
removeMouseTrackListener(mouseHoverHandler);
}
} // MapCanvas