/*
* Copyright 2008 Google Inc.
*
* 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 com.google.speedtracer.client.view;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Style;
import com.google.gwt.graphics.client.Canvas;
import com.google.gwt.graphics.client.Color;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.topspin.ui.client.Window;
import com.google.speedtracer.client.model.GraphCalloutModel;
import com.google.speedtracer.client.timeline.TimeLineModel;
import com.google.speedtracer.client.util.TimeStampFormatter;
import com.google.speedtracer.client.util.dom.DocumentExt;
/**
* Marker that represents the current selection.
*
* Draws itself as a leader pointing up and to the right with a label trailing
* until it approaches the right hand side of the window. At that point, the
* label points toward the right. If the value being annotated is too high, the
* label is drawn over the bottom of the map instead of over the top.
*/
public class GraphCallout implements GraphCalloutModel.ChangeListener {
/**
* CSS.
*/
public interface Css extends CssResource {
String duration();
String icon();
String label();
int labelHeight();
String lead();
int leadHeight();
int leadWidth();
}
/**
* Externalized interface.
*/
public interface Resources extends ClientBundle {
@Source("resources/GraphCallout.css")
GraphCallout.Css currentSelectionMarkerCss();
}
private static final int MINIMUM_DURATION_DISPLAY_WIDTH = 1;
private final Canvas canvas;
private final Css css;
private final Element element;
private final Element label;
private final int reverseTippingPoint = 250;
private final MainTimeLine mainTimeline;
private final Element parentElement;
public GraphCallout(GraphCalloutModel calloutModel,
MainTimeLine mainTimeline, GraphCallout.Resources resources) {
this.css = resources.currentSelectionMarkerCss();
this.mainTimeline = mainTimeline;
this.parentElement = mainTimeline.getGraphContainerElement();
// Create a canvas to draw the leader.
canvas = new Canvas(css.leadWidth(), css.leadHeight());
canvas.getElement().setClassName(css.lead());
// Add containers for the label text and the duration box.
DocumentExt document = parentElement.getOwnerDocument().cast();
label = document.createDivWithClassName(css.label());
element = document.createDivWithClassName(css.duration());
element.appendChild(canvas.getElement());
element.appendChild(label);
parentElement.appendChild(element);
calloutModel.addModelChangeListener(this);
}
public void onModelChange(GraphCalloutModel m) {
canvas.clear();
if (m.isSelected()) {
TimeLineModel timelineModel = mainTimeline.getModel();
double pixelsPerDomain = mainTimeline.getCurrentGraphWidth()
/ (timelineModel.getRightBound() - timelineModel.getLeftBound());
int xPosition = (int) ((m.getStartTime() - timelineModel.getLeftBound()) * pixelsPerDomain);
// Compute the duration as pixels, and only display the
// duration block if it exceeds a minimum size.
double durationPixels = m.getDuration() * pixelsPerDomain;
label.setInnerHTML(m.getDescription() + " @"
+ TimeStampFormatter.format(m.getStartTime()));
paint(xPosition, (int) durationPixels);
} else {
label.setInnerHTML("");
element.getStyle().setProperty("display", "none");
}
}
private void paint(int xPosition, int durationPixels) {
canvas.setStrokeStyle(Color.BLACK);
canvas.setLineWidth(2.0);
canvas.beginPath();
// Determine which horizontal direction the leader should be drawn.
int distanceFromRight = Window.getInnerWidth()
- (parentElement.getAbsoluteLeft() + xPosition);
boolean reverseLeader = distanceFromRight < reverseTippingPoint;
Style canvasStyle = canvas.getElement().getStyle();
Style labelStyle = label.getStyle();
Style durationStyle = element.getStyle();
// If the duration div runs off of the left, it overwrites the
// title, so fix that up.
if (xPosition <= 0) {
durationPixels += xPosition;
xPosition = 0;
durationStyle.setProperty("borderLeft", "none");
} else {
durationStyle.setProperty("borderLeft", "1px solid #000");
}
// Set the width of the duration div
if (reverseLeader) {
durationPixels = Math.min(distanceFromRight, durationPixels);
}
durationStyle.setProperty("display", "block");
durationStyle.setPropertyPx("width", Math.max(durationPixels,
MINIMUM_DURATION_DISPLAY_WIDTH));
durationStyle.setPropertyPx("left", xPosition);
if (reverseLeader) {
// Close to the right margin, so flip the direction of the leader
// draw the vertical part on the right side of the canvas.
labelStyle.setPropertyPx("left",
-(css.leadWidth() + label.getOffsetWidth()));
canvasStyle.setPropertyPx("left", -css.leadWidth());
// Draw the leader
canvas.moveTo(css.leadWidth(), css.leadHeight());
canvas.setLineWidth(1.0);
canvas.lineTo(0, 0);
} else {
// Draw the vertical part of the leader on the left side of the canvas.
// If the xPosition would run into the scale label, move it over a little.
int labelOffset = css.leadWidth() + ((xPosition <= 20) ? 20 : 0);
labelStyle.setPropertyPx("left", labelOffset);
canvasStyle.setPropertyPx("left", 0);
// Draw the leader
canvas.moveTo(0, css.leadHeight());
canvas.setLineWidth(0.5);
canvas.lineTo(css.leadWidth(), 0);
}
canvas.stroke();
}
}