/*
* Copyright 2003-2010 Tufts University Licensed under the
* Educational Community 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.osedu.org/licenses/ECL-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 tufts.vue;
import java.io.File;
import java.util.List;
import java.util.*;
import java.awt.*;
import java.awt.geom.Point2D;
import java.awt.geom.Line2D;
import java.awt.geom.AffineTransform;
import java.awt.geom.PathIterator;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.JColorChooser;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import edu.tufts.vue.metadata.MetadataList;
import edu.tufts.vue.metadata.VueMetadataElement;
import tufts.vue.VueResources;
import tufts.vue.LWComponent.ChildKind;
/**
*
* Various static utility methods for VUE.
*
* @version $Revision: 1.110 $ / $Date: 2010-05-21 18:44:08 $ / $Author: brian $
* @author Scott Fraize
*
*/
public class VueUtil extends tufts.Util
{
private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(VueUtil.class);
public static final String DEFAULT_WINDOWS_FOLDER = "vue_2";
public static final String DEFAULT_MAC_FOLDER = ".vue_2";
public static final String VueExtension = VueResources.getString("vue.extension", ".vue");
public static final String VueArchiveExtension = VueResources.getString("vue.archive.extension", ".vpk");
private static String currentDirectoryPath = "";
public static void openURL(String platformURL)
throws java.io.IOException
{
boolean isMailto = false;
String logURL = platformURL;
if (platformURL != null && platformURL.startsWith("mailto:")) {
isMailto = true;
if (platformURL.length() > 80) {
// in case there's a big subject or body (e.g, ?subject=Foo&body=Bar in the URL), don't log the whole thing
logURL = platformURL.substring(0,80) + "...";
}
Log.info("openURL[" + logURL + "]");
} else
Log.debug("openURL[" + logURL + "]");
if (isMacPlatform() && VUE.inNativeFullScreen())
tufts.vue.gui.FullScreen.dropFromNativeToWorking();
else if (isUnixPlatform() && VUE.inNativeFullScreen())
tufts.vue.gui.FullScreen.dropFromNativeToFrame();
// todo: spawn this in another thread just in case it hangs
if (!isMailto) {
String lowCaseURL = platformURL.toLowerCase();
if (lowCaseURL.endsWith(VueExtension) ||
lowCaseURL.endsWith(VueArchiveExtension) ||
lowCaseURL.endsWith(".zip") ||
(DEBUG.Enabled && lowCaseURL.endsWith(".xml")))
{
if (lowCaseURL.startsWith("resource:")) {
// Special case for startup.vue which can be embedded in the classpath
java.net.URL url = VueResources.getURL(platformURL.substring(9));
VUE.displayMap(tufts.vue.action.OpenAction.loadMap(url));
return;
}
final File file = Resource.getLocalFileIfPresent(platformURL);
if (file != null) {
// TODO: displayMap should be changed to take either a URL or a random url/path spec-string,
// NOT a local file, as we can open maps at the other end of HTTP url's, and we need an
// object that abstracts both.
tufts.vue.VUE.displayMap(file);
} else {
final LWMap loadMap = tufts.vue.action.OpenAction.loadMap(new java.net.URL(platformURL));
tufts.vue.VUE.displayMap(loadMap);
loadMap.setFile(null);
}
// try {
// File file = new File(new java.net.URL(platformURL).getFile());
// if(file.exists()) {
// tufts.vue.VUE.displayMap(file);
// } else{
// LWMap loadMap = tufts.vue.action.OpenAction.loadMap(new java.net.URL(platformURL));
// tufts.vue.VUE.displayMap(loadMap);
// loadMap.setFile(null);
// }
// } catch (java.net.MalformedURLException e) {
// Log.error(e + " " + platformURL);
// e.printStackTrace();
// try {
// tufts.vue.VUE.displayMap(new File(platformURL));
// } catch (Exception ex) {
// System.out.println(ex + " " + platformURL);
// tufts.Util.openURL(platformURL);
// }
// } catch(Exception ex) {
// ex.printStackTrace();
// }
return;
}
}
if (VUE.isApplet()) {
java.net.URL url = null;
try {
url = new java.net.URL(platformURL);
System.out.println("Applet URL display: " + url);
VUE.getAppletContext().showDocument(url, "_blank");
} catch (Exception e) {
e.printStackTrace();
}
} else {
// already handled in Util.openURL
//if (isMacPlatform() && platformURL.startsWith("/"))
// platformURL = "file:" + platformURL;
tufts.Util.openURL(platformURL);
}
}
public static void setCurrentDirectoryPath(String cdp) {
currentDirectoryPath = cdp;
}
public static String getCurrentDirectoryPath() {
return currentDirectoryPath;
}
public static boolean isCurrentDirectoryPathSet() {
if(currentDirectoryPath.equals(""))
return false;
else
return true;
}
public static File getDefaultUserFolder() {
File userHome = null;
if (VUE.isApplet())
userHome = new File(VUE.getSystemProperty("user.home"));
else
{
String userHomeString = System.getenv("VUEUSERHOME");
if (userHomeString ==null || (userHomeString !=null && userHomeString.length() <1))
userHome = new File(VUE.getSystemProperty("user.home"));
else
userHome = new File(userHomeString);
}
if(userHome == null)
userHome = new File(VUE.getSystemProperty("java.io.tmpdir"));
final String vueUserDir = isWindowsPlatform() ? DEFAULT_WINDOWS_FOLDER : DEFAULT_MAC_FOLDER;
File userFolder = new File(userHome.getPath() + File.separatorChar + vueUserDir);
if(userFolder.isDirectory())
return userFolder;
if(!userFolder.mkdir())
throw new RuntimeException(userFolder.getAbsolutePath()+":cannot be created");
return userFolder;
}
public static void deleteDefaultUserFolder() {
File userFolder = getDefaultUserFolder();
File[] files = userFolder.listFiles();
System.out.println("file count = "+files.length);
for(int i = 0; i<files.length;i++) {
if(files[i].isFile() && !files[i].delete())
throw new RuntimeException(files[i].getAbsolutePath()+":cannot be created");
}
if(!userFolder.delete())
throw new RuntimeException(userFolder.getAbsolutePath()+":cannot be deleted");
}
public static void copyURL(java.net.URL url, java.io.File file)
throws java.io.IOException
{
if (DEBUG.IO) out("VueUtil: copying " + url + " to " + file);
copyStream(url.openStream(), new java.io.FileOutputStream(file));
}
public static void copyStream(java.io.InputStream in, java.io.OutputStream out)
throws java.io.IOException
{
int len = 0;
byte[] buf = new byte[8192];
while ((len = in.read(buf)) != -1) {
if (DEBUG.IO) out("VueUtil: copied " + len + " to " + out);
out.write(buf, 0, len);
}
in.close();
out.close();
}
/**
* Compute the intersection point of two lines, as defined
* by two given points for each line.
* This already assumes that we know they intersect somewhere (are not parallel),
*/
public static float[] computeLineIntersection
(float s1x1, float s1y1, float s1x2, float s1y2,
float s2x1, float s2y1, float s2x2, float s2y2, float[] result)
{
// We are defining a line here using the formula:
// y = mx + b -- m is slope, b is y-intercept (where crosses x-axis)
final boolean m1vertical = (Math.abs(s1x1 - s1x2) < 0.001f);
final boolean m2vertical = (Math.abs(s2x1 - s2x2) < 0.001f);
final float m1;
final float m2;
if (!m1vertical)
m1 = (s1y1 - s1y2) / (s1x1 - s1x2);
else
m1 = Float.NaN;
if (!m2vertical)
m2 = (s2y1 - s2y2) / (s2x1 - s2x2);
else
m2 = Float.NaN;
// Solve for b using any two points from each line.
// to solve for b:
// y = mx + b
// y + -b = mx
// -b = mx - y
// b = -(mx - y)
// float b1 = -(m1 * s1x1 - s1y1);
// float b2 = -(m2 * s2x1 - s2y1);
// System.out.println("m1=" + m1 + " b1=" + b1);
// System.out.println("m2=" + m2 + " b2=" + b2);
// if EITHER line is vertical, the x value of the intersection
// point will obviously have to be the x value of any point
// on the vertical line.
float x = 0;
float y = 0;
if (m1vertical) { // first line is vertical
//System.out.println("setting X to first vertical at " + s1x1);
float b2 = -(m2 * s2x1 - s2y1);
x = s1x1; // set x to any x point from the first line
// using y=mx+b, compute y using second line
y = m2 * x + b2;
} else {
float b1 = -(m1 * s1x1 - s1y1);
if (m2vertical) { // second line is vertical (has no slope)
//System.out.println("setting X to second vertical at " + s2x1);
x = s2x1; // set x to any point from the second line
} else {
// second line has a slope (is not veritcal: m is valid)
float b2 = -(m2 * s2x1 - s2y1);
x = (b2 - b1) / (m1 - m2);
}
// using y=mx+b, compute y using first line
y = m1 * x + b1;
}
//System.out.println("x=" + x + " y=" + y);
result[0] = x;
result[1] = y;
return result;
}
public static final float[] NoIntersection = { Float.NaN, Float.NaN, Float.NaN, Float.NaN };
private static final String[] SegTypes = { "MOVEto", "LINEto", "QUADto", "CUBICto", "CLOSE" }; // for debug
public static float[] computeIntersection(float rayX1, float rayY1,
float rayX2, float rayY2,
java.awt.Shape shape, java.awt.geom.AffineTransform shapeTransform)
{
return computeIntersection(rayX1,rayY1, rayX2,rayY2, shape, shapeTransform, new float[2], 1);
}
public static Point2D.Float computeIntersection(Line2D.Float l, LWComponent c) {
float[] p = computeIntersection(l.x1, l.y1, l.x2, l.y2, c.getZeroShape(), c.getZeroTransform(), new float[2], 1);
return new Point2D.Float(p[0], p[1]);
}
public static float[] computeIntersection(float segX1, float segY1, float segX2, float segY2, LWComponent c) {
return computeIntersection(segX1, segY1, segX2, segY2, c.getZeroShape(), c.getZeroTransform(), new float[2], 1);
}
/**
* Compute the intersection of an arbitrary shape and a line segment
* that is assumed to pass throught the shape. Usually used
* with an endpoint (rayX2,rayY2) that ends in the center of the
* shape, tho that's not required.
*
* @param max - max number of intersections to compute. An x/y
* pair of coords will put into result up to max times. Must be >= 1.
*
* @return float array of size 2: x & y values of intersection,
* or ff no intersection, returns Float.NaN values for x/y.
*/
public static float[] computeIntersection(float segX1, float segY1,
float segX2, float segY2,
java.awt.Shape shape, java.awt.geom.AffineTransform shapeTransform,
float[] result, int max)
{
java.awt.geom.PathIterator i = shape.getPathIterator(shapeTransform);
// todo performance: if this shape has no curves (CUBICTO or QUADTO)
// this flattener is redundant. Also, it would be faster to
// actually do the math for arcs and compute the intersection
// of the arc and the line, tho we can save that for another day.
i = new java.awt.geom.FlatteningPathIterator(i, 0.5);
float[] seg = new float[6];
float firstX = 0f;
float firstY = 0f;
float lastX = 0f;
float lastY = 0f;
int cnt = 0;
int hits = 0;
while (!i.isDone()) {
int segType = i.currentSegment(seg);
if (cnt == 0) {
firstX = seg[0];
firstY = seg[1];
} else if (segType == PathIterator.SEG_CLOSE) {
seg[0] = firstX;
seg[1] = firstY;
}
float endX = seg[0];
float endY = seg[1];
// at cnt == 0, we have only the first point from the path iterator, and so no line yet.
if (cnt > 0 && Line2D.linesIntersect(segX1, segY1, segX2, segY2, lastX, lastY, seg[0], seg[1])) {
//System.out.println("intersection at segment #" + cnt + " " + SegTypes[segType]);
if (max <= 1) {
return computeLineIntersection(segX1, segY1, segX2, segY2, lastX, lastY, seg[0], seg[1], result);
} else {
float[] tmp = computeLineIntersection(segX1, segY1, segX2, segY2, lastX, lastY, seg[0], seg[1], new float[2]);
result[hits*2 + 0] = tmp[0];
result[hits*2 + 1] = tmp[1];
if (++hits >= max)
return result;
}
}
cnt++;
lastX = endX;
lastY = endY;
i.next();
}
return NoIntersection;
}
/** compute the first two y value crossings of the given x_axis and shape */
public static float[] computeYCrossings(float x_axis, Shape shape, float[] result) {
return computeIntersection(x_axis, Integer.MIN_VALUE, x_axis, Integer.MAX_VALUE, shape, null, result, 2);
}
/** compute 2 y values for crossings of at x_axis, and store result in the given Line2D */
public static Line2D computeYCrossings(float x_axis, Shape shape, Line2D result) {
float[] coords = computeYCrossings(x_axis, shape, new float[4]);
result.setLine(x_axis, coords[1], x_axis, coords[3]);
return result;
}
/**
* This will clip the given vertical line to the edges of the given shape.
* Assumes line start is is min y (top), line end is max y (bottom).
* @param line - line to clip y values if outside edge of given shape
* @param shape - shape to clip line to
* @param pad - padding: keep line endpoints at least this many units away from shape edge
*
* todo: presumes only 2 crossings: will only handle concave polygons
* Should be relatively easy to extend this to work for non-vertical lines if the need arises.
*/
public static Line2D clipToYCrossings(Line2D line, Shape shape, float pad)
{
float x_axis = (float) line.getX1();
float[] coords = computeYCrossings(x_axis, shape, new float[4]);
// coords[0] & coords[2], the x values, can be ignored, as they always == x_axis
if (coords.length < 4) {
// TODO FIX: if line is outside edge of shape, we're screwed (see d:/test-layout.vue)
// TODO: we were getting this of NoIntersection being returned (which was only of size
// 2, and thus give us array bounds exceptions below) -- do we need to do anything
// here to make sure the NoIntersection case is handled more smoothly?
System.err.println("clip error " + coords);
new Throwable("CLIP ERROR shape=" + shape).printStackTrace();
return null;
}
float upper; // y value at top
float lower; // y value at bottom
if (coords[1] < coords[3]) {
// cross1 is min cross (top), cross2 is max cross (bottom)
upper = coords[1];
lower = coords[3];
} else {
// cross2 is min cross (top), cross1 is max cross (bottom)
upper = coords[3];
lower = coords[1];
}
upper += pad;
lower -= pad;
// clip line to upper & lower (top & bottom)
float y1 = Math.max(upper, (float) line.getY1());
float y2 = Math.min(lower, (float) line.getY2());
line.setLine(x_axis, y1, x_axis, y2);
return line;
}
/** clip the given amount of length off each end of the given line -- negative values will extend the line length */
public static Line2D.Float clipEnds(final Line2D.Float line, final double clipLength)
{
final double rise = line.y1 - line.y2; // delta Y
final double run = line.x1 - line.x2; // delta X
final double slope = run / rise; // inverse slope is what works here: due to +y is down in coord system?
final double theta = Math.atan(slope);
final double clipX = Math.sin(theta) * clipLength;
final double clipY = Math.cos(theta) * clipLength;
if (DEBUG.PATHWAY) {
out("\nLine: " + fmt(line) + " clipping lenth off ends: " + clipLength);
out(String.format("XD %.1f YD %.1f Slope %.1f Theta %.2f clipX %.1f clipY %.1f", run, rise, slope, theta, clipX, clipY));
}
if (line.y1 < line.y2) {
line.x1 += clipX;
line.x2 -= clipX;
line.y1 += clipY;
line.y2 -= clipY;
} else {
line.x1 -= clipX;
line.x2 += clipX;
line.y1 -= clipY;
line.y2 += clipY;
}
return line;
}
public static Line2D.Float computeConnector(LWComponent c1, LWComponent c2, Line2D.Float result)
{
computeConnectorAndCenterHit(c1, c2, result);
return result;
}
//public static Line2D.Float computeConnector(LWComponent c1, LWComponent c2, Line2D.Float result)
/**
* On a line drawn from the center of head to the center of tail, compute the the line segment
* from the intersection at the edge of shape head to the intersection at the edge of shape tail.
* The returned line will be in the LWMap coordinate space. If the components overlap sufficiently,
* the segment returned will either be from the center of one component to the edge of the other,
* or from center-to-center.
*
* @param result: this line will be set to the connecting segment
* @return true if the components overlapped in such a way as to cause the segment to connect at one or
* or both of the component centers, as opposed to their edges
*/
public static boolean computeConnectorAndCenterHit(LWComponent head, LWComponent tail, Line2D.Float result)
{
// TODO: do these defaults still want to be the map-center now that we do
// relative coords and parent-local links? Shouldn't they be the center
// relative to some desired parent focal? (e.g. a link parent)
final float headX = head.getMapCenterX();
final float headY = head.getMapCenterY();
final float tailX = tail.getMapCenterX();
final float tailY = tail.getMapCenterY();
// compute intersection at head shape of line from center of head to center of tail shape
final float[] intersection_at_1 = computeIntersection(headX, headY, tailX, tailY, head);
boolean overlap = false;
if (intersection_at_1 == NoIntersection) {
// default to center of component 1
result.x1 = headX;
result.y1 = headY;
overlap = true;
} else {
result.x1 = intersection_at_1[0];
result.y1 = intersection_at_1[1];
}
// compute intersection at tail shape of line from prior intersection to center of tail shape
final float[] intersection_at_2 = computeIntersection(result.x1, result.y1, tailX, tailY, tail);
if (intersection_at_2 == NoIntersection) {
// default to center of component 2
result.x2 = tailX;
result.y2 = tailY;
overlap = true;
} else {
result.x2 = intersection_at_2[0];
result.y2 = intersection_at_2[1];
}
return overlap;
}
// Old version: could produce "internal" connections if nodes overlapped: directionality of the connector
// would get reversed. E.g., connector would be from the edge of a node back towards it's own center,
// to connect the outer edge of an overlapping node.
// public static boolean computeConnectorAndCenterHit(LWComponent c1, LWComponent c2, Line2D.Float result)
// {
// // TODO: do these defaults still want to be the map-center now that we do
// // relative coords and parent-local links? Shouldn't they be the center
// // relative to some desired parent focal? (e.g. a link parent)
// final float segX1 = c1.getMapCenterX();
// final float segY1 = c1.getMapCenterY();
// final float segX2 = c2.getMapCenterX();
// final float segY2 = c2.getMapCenterY();
// // compute intersection at shape 1 of line from center of shape 1 to center of shape 2
// final float[] intersection_at_1 = computeIntersection(segX1, segY1, segX2, segY2, c1);
// // compute intersection at shape 2 of line from center of shape 2 to center of shape 1
// final float[] intersection_at_2 = computeIntersection(segX2, segY2, segX1, segY1, c2);
// boolean overlap = false;
// if (intersection_at_1 == NoIntersection) {
// // default to center of component 1
// result.x1 = segX1;
// result.y1 = segY1;
// overlap = true;
// } else {
// result.x1 = intersection_at_1[0];
// result.y1 = intersection_at_1[1];
// }
// if (intersection_at_2 == NoIntersection) {
// // default to center of component 2
// result.x2 = segX2;
// result.y2 = segY2;
// overlap = true;
// } else {
// result.x2 = intersection_at_2[0];
// result.y2 = intersection_at_2[1];
// }
// //System.out.println("connector: " + out(result));
// //System.out.println("\tfrom: " + c1);
// //System.out.println("\t to: " + c2);
// return overlap;
// }
public static double computeVerticalRotation(Line2D l) {
return computeVerticalRotation(l.getX1(), l.getY1(), l.getX2(), l.getY2());
}
/**
* Compute the rotation needed to normalize the line segment to vertical orientation, making it
* parrallel to the Y axis. So vertical lines will return either 0 or Math.PI (180 degrees), horizontal lines
* will return +/- PI/2. (+/- 90 degrees). In the rotated space, +y values will move down, +x values will move right.
*/
public static double computeVerticalRotation(double x1, double y1, double x2, double y2)
{
final double xdiff = x1 - x2;
final double ydiff = y1 - y2;
final double slope = xdiff / ydiff; // really, inverse slope
double radians = -Math.atan(slope);
if (xdiff >= 0 && ydiff >= 0)
radians += Math.PI;
else if (xdiff <= 0 && ydiff >= 0)
radians -= Math.PI;
return radians;
}
/**
* Move a point a given distance along a line parallel to the
* ray implied by the the given line. The direction of projection
* is parallel to the ray that begins at the first point in the line,
* and passes through the second point of the line. The start point
* does not need to be on the given line.
*
* @return the new point
*/
public static Point2D projectPoint(float x, float y, Line2D ray, float distance) {
// todo: this impl could be much simpler
final Point2D.Float p = new Point2D.Float();
final double rotation = computeVerticalRotation(ray);
final java.awt.geom.AffineTransform tx = new java.awt.geom.AffineTransform();
tx.setToTranslation(x, y);
tx.rotate(rotation);
tx.translate(0,distance);
tx.transform(p,p);
return p;
}
/**
* @return the point at the "center" of all the given nodes. If the given
* collection is null or contains no elements, null is returned. If the given
* collection contains only one element, the center point of that element is
* returned. The returned point is in coordinates at the top level map. E.g., even
* if all the nodes are children of a slide, the returned coordinate will not be
* relative to the slide, it will be relative to the map.
*/
public static Point2D.Float computeCentroid(Collection<LWComponent> nodes)
{
if (nodes == null || nodes.isEmpty())
return null;
float sumX = 0, sumY = 0;
int count = 0;
for (LWComponent c : nodes) {
final float cx = c.getMapX() + c.getMapWidth() / 2;
final float cy = c.getMapY() + c.getMapHeight() / 2;
sumX += cx;
sumY += cy;
count++;
}
return new Point2D.Float(sumX / count,
sumY / count);
}
public static Point2D projectPoint(Point2D.Float p, Line2D ray, float distance) {
return projectPoint(p.x, p.y, ray, distance);
}
public static void dumpBytes(String s) {
try {
dumpBytes(s.getBytes("UTF-8"));
} catch (Exception e) {
e.printStackTrace();
}
}
public static void dumpBytes(byte[] bytes) {
for (int i = 0; i < bytes.length; i++) {
byte b = bytes[i];
System.out.println("byte " + (i<10?" ":"") + i
+ " (" + ((char)b) + ")"
+ " " + pad(' ', 4, new Byte(b).toString())
+ " " + pad(' ', 2, Integer.toHexString( ((int)b) & 0xFF))
+ " " + pad('X', 8, toBinary(b))
);
}
}
public static String toBinary(byte b) {
StringBuffer buf = new StringBuffer(8);
buf.append((b & (1<<7)) == 0 ? '0' : '1');
buf.append((b & (1<<6)) == 0 ? '0' : '1');
buf.append((b & (1<<5)) == 0 ? '0' : '1');
buf.append((b & (1<<4)) == 0 ? '0' : '1');
buf.append((b & (1<<3)) == 0 ? '0' : '1');
buf.append((b & (1<<2)) == 0 ? '0' : '1');
buf.append((b & (1<<1)) == 0 ? '0' : '1');
buf.append((b & (1<<0)) == 0 ? '0' : '1');
return buf.toString();
}
public static void dumpString(String s) {
char[] chars = s.toCharArray();
for (int i = 0; i < chars.length; i++) {
int cv = (int) chars[i];
System.out.println("char " + (i<10?" ":"") + i
+ " (" + chars[i] + ")"
+ " " + pad(' ', 6, new Integer(cv).toString())
+ " " + pad(' ', 4, Integer.toHexString(cv))
+ " " + pad('0', 16, Integer.toBinaryString(cv))
);
}
}
public static Map getQueryData(String query) {
String[] pairs = query.split("&");
Map map = new HashMap();
for (int i = 0; i < pairs.length; i++) {
String pair = pairs[i];
if (DEBUG.DATA || DEBUG.IMAGE) System.out.println("query pair " + pair);
int eqIdx = pair.indexOf('=');
if (eqIdx > 0) {
String key = pair.substring(0, eqIdx);
String value = pair.substring(eqIdx+1, pair.length());
map.put(key.toLowerCase(), value);
}
}
return map;
}
public static boolean isTransparent(Color c) {
return c == null || c.getAlpha() == 0;
}
public static boolean isTranslucent(Color c) {
return c == null || c.getAlpha() != 0xFF;
}
public static void alert(Component parent, Object message, String title) {
VOptionPane.showWrappingMessageDialog(parent,
message,
title,
JOptionPane.ERROR_MESSAGE,
VueResources.getImageIcon("vueIcon32x32"));
}
public static void alert(Component parent, Object message, String title, int messageType) {
VOptionPane.showWrappingMessageDialog(parent,
message,
title,
messageType,
null);
}
public static void alert(String title, Throwable t) {
java.io.Writer buf = new java.io.StringWriter();
t.printStackTrace(new java.io.PrintWriter(buf));
JComponent msg = new JTextArea(buf.toString());
msg.setOpaque(false);
msg.setFont(new Font("Lucida Grande", Font.PLAIN, 9));
VOptionPane.showWrappingMessageDialog(VUE.getDialogParent(),
msg,
title,
JOptionPane.ERROR_MESSAGE,
VueResources.getImageIcon("vueIcon32x32"));
}
public static void alert(Object message, String title) {
VOptionPane.showWrappingMessageDialog(VUE.getDialogParent(),
message,
title,
JOptionPane.ERROR_MESSAGE,
VueResources.getImageIcon("vueIcon32x32"));
}
public static int confirm(Object message, String title) {
return VOptionPane.showWrappingConfirmDialog(VUE.getDialogParent(),
message,
title,
JOptionPane.YES_NO_OPTION,
JOptionPane.QUESTION_MESSAGE,
VueResources.getImageIcon("vueIcon32x32"));
}
public static int confirm(Component parent, Object message, String title) {
return VOptionPane.showWrappingConfirmDialog(parent,
message,
title,
JOptionPane.YES_NO_OPTION,
JOptionPane.QUESTION_MESSAGE,
VueResources.getImageIcon("vueIcon32x32"));
}
public static int confirm(Component parent, Object message, String title, int optionType) {
return VOptionPane.showWrappingConfirmDialog(parent,
message,
title,
optionType,
JOptionPane.QUESTION_MESSAGE,
null);
}
public static int confirm(Component parent, Object message, String title, int optionType, int messageType) {
return VOptionPane.showWrappingConfirmDialog(parent,
message,
title,
optionType,
messageType,
null);
}
public static int option(Component parent, Object message, String title, int optionType, int messageType, Object[] options, Object initialValue) {
return VOptionPane.showWrappingOptionDialog(parent,
message,
title,
optionType,
messageType,
null,
options,
initialValue);
}
public static Object input(Object message) {
return VOptionPane.showWrappingInputDialog(null,
message,
null,
JOptionPane.QUESTION_MESSAGE,
null,
null,
null);
}
public static Object input(Component parent, Object message, String title, int messageType,
Object[] selectionValues, Object initialSelectionValue) {
return VOptionPane.showWrappingInputDialog(parent,
message,
title,
messageType,
null,
selectionValues,
initialSelectionValue);
}
public static int getMaxLabelLineLength() {
// todo: this should be cached
return VueResources.getInt("dataNode.labelLength");
}
private static JColorChooser colorChooser;
private static Dialog colorChooserDialog;
private static boolean colorChosen;
/** Convience method for running a JColorChooser and collecting the result */
public static Color runColorChooser(String title, java.awt.Color c, java.awt.Component chooserParent)
{
if (colorChooserDialog == null) {
colorChooser = new JColorChooser();
//colorChooser.setDragEnabled(true);
//colorChooser.setPreviewPanel(new JLabel("FOO")); // makes it dissapear entirely, W2K/1.4.2/Metal
if (false) {
final JPanel np = new JPanel();
np.add(new JLabel(VueResources.getString("jlabel.text")));
np.setSize(new Dimension(300,100)); // will be invisible otherwise
np.setBackground(Color.red);
//np.setBorder(new EmptyBorder(10,10,10,10));
//np.setBorder(new EtchedBorder());
np.setBorder(new LineBorder(Color.black));
np.setOpaque(true);
np.addPropertyChangeListener(new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent e) {
System.out.println("CC " + e.getPropertyName() + "=" + e.getNewValue());
if (e.getPropertyName().equals("foreground"))
np.setBackground((Color)e.getNewValue());
}});
colorChooser.setPreviewPanel(np); // also makes dissapear entirely
}
/*
JComponent pp = colorChooser.getPreviewPanel();
System.out.println("CC Preview Panel: " + pp);
for (int i = 0; i < pp.getComponentCount(); i++)
System.out.println("#" + i + " " + pp.getComponent(i));
colorChooser.getPreviewPanel().add(new JLabel("FOO"));
*/
colorChooserDialog =
JColorChooser.createDialog(chooserParent,
VueResources.getString("dialog.colorchooser.title"),
true,
colorChooser,
new ActionListener() { public void actionPerformed(ActionEvent e)
{ colorChosen = true; } },
null);
}
if (c != null)
colorChooser.setColor(c);
if (title != null)
colorChooserDialog.setTitle(title);
colorChosen = false;
// show() blocks until a color chosen or cancled, then automatically hides the dialog:
colorChooserDialog.setVisible(true);
JComponent pp = colorChooser.getPreviewPanel();
System.out.println("CC Preview Panel: " + pp + " children=" + Arrays.asList(pp.getComponents()));
for (int i = 0; i < pp.getComponentCount(); i++)
System.out.println("#" + i + " " + pp.getComponent(i));
return colorChosen ? colorChooser.getColor() : null;
}
//----------------------------------------------------------------------------------------
// Below generic relational clustiner code by Anoop -- refactored by SMF:
//----------------------------------------------------------------------------------------
private static final boolean ALL_DATA = true; // use all data while comparing similarity between two LW Components. All includes notes and metadata
public static void setXYByClustering(LWNode node) {
setXYByClustering(Collections.singletonList(node));
}
public static List<LWComponent> setXYByClustering(Collection<? extends LWComponent> layoutNodes) {
return setXYByClustering(tufts.vue.VUE.getActiveMap(),
layoutNodes);
}
public static List<LWComponent> setXYByClustering(LWMap map, Collection<? extends LWComponent> layoutNodes)
{
final Collection<LWComponent> all = map.getAllDescendents();
final Collection<LWNode> relatingNodes = new ArrayList(all.size() / 2);
for (LWNode n : typeFilter(all, LWNode.class)) {
if (!layoutNodes.contains(n))
relatingNodes.add(n);
}
final List<LWComponent> untouched = new ArrayList();
for (LWComponent c : layoutNodes) {
try {
// performance: pre-compute top-level-items for possible pushing and pass it in here:
if (!setXYByClustering(map, relatingNodes, c))
untouched.add(c);
} catch (Throwable t) {
Log.warn("weighted cluster failed for " + c, t);
}
}
return untouched;
}
/** relations should NOT contain the node at this point */
private static boolean setXYByClustering(LWMap map, Collection<LWNode> relations, LWComponent node)
{
Log.debug("relating to " + tags(relations) + ": " + node);
float xNumerator = 0 ;
float yNumerator = 0 ;
float denominator = 0 ;
for (LWNode mapNode : relations) {
double score = computeScore(node, mapNode);
xNumerator += score*score*mapNode.getX();
yNumerator += score*score*mapNode.getY();
denominator += score*score;
}
if (denominator != 0) {
float x = xNumerator/denominator;
float y = yNumerator/denominator;
node.setX(x);
node.setY(y);
for (LWComponent mapNode : relations) {
if (checkCollision(mapNode, node)) {
// ideally, we'd pre-fetch the list of all top-level items to
// push -- projectNodes is going to refetch them for every push:
try {
//Actions.projectNodes(node, 24, Actions.PUSH_ALL);
// performance: pass in pre-computed top-level-items to push, not the map
Actions.projectNodes(map.getTopLevelItems(ChildKind.EDITABLE), node, 24);
} catch (Throwable t) {
Log.warn("projection failure " + node, t);
}
}
}
return true;
} else
return false;
}
public static double computeScore (LWComponent n1, LWComponent n2) {
double score = 0.0;
String content1 = n1.getLabel();
String content2 = n1.getLabel();
if(ALL_DATA) {
content1 += " "+n1.getNotes();
content2 += " "+n2.getNotes();
if(n1.getResource()!= null) content1 += " "+n1.getResource().getSpec();
if(n2.getResource()!= null) content2 += " "+n2.getResource().getSpec();
MetadataList mList1 = n1.getMetadataList();
for(VueMetadataElement vme: mList1.getMetadata()){
content1 +=" "+vme.getKey();
content1 +=" "+vme.getValue();
}
MetadataList mList2 = n2.getMetadataList();
for(VueMetadataElement vme: mList2.getMetadata()){
content2 +=" "+vme.getKey();
content2 +=" "+vme.getValue();
}
}
String[] words1 = content1.split("\\s+");
String[] words2 = content2.split("\\s+");
int matches = 0;
for(int i = 0;i<words1.length;i++) {
if(n2.getLabel().contains(words1[i])){
matches++;
}
}
double p1 = (double) matches / words1.length;
double p2 = (double) matches/words2.length;
if(p1== 0 && p2 == 0 ){
score = 0.0;
} else {
score = 2*p1*p2/(p1+p2); // harmonic mean
}
return score;
}
public static boolean checkCollision(LWComponent c1, LWComponent c2)
{
boolean collide = false;
if(c2.getX()>= c1.getX() && c2.getX() <= c1.getX()+c1.getWidth() && c2.getY() >= c1.getY() && c2.getY() <=c1.getY()+c2.getHeight()) {
collide = true;
}
return collide;
}
}
/**
* VOptionPane extends JOptionPane for the sole purpose of returning MAX_LINE_LENGTH
* from getMaxCharactersPerLineCount() so that long messages will wrap.
*/
class VOptionPane extends JOptionPane
{
static final long serialVersionUID = 1;
static final int MAX_LINE_LENGTH = 80;
VOptionPane() {
}
public int getMaxCharactersPerLineCount() {
return MAX_LINE_LENGTH;
}
static void showWrappingMessageDialog(Component parent, Object message, String title,
int messageType, Icon icon)
throws HeadlessException{
showWrappingOptionDialog(parent, message, title, JOptionPane.DEFAULT_OPTION, messageType, icon, null, null);
}
static int showWrappingConfirmDialog(Component parent, Object message, String title,
int optionType, int messageType, Icon icon)
throws HeadlessException {
return showWrappingOptionDialog(parent, message, title, optionType, messageType, icon, null, null);
}
static int showWrappingOptionDialog(Component parent, Object message, String title,
int optionType, int messageType, Icon icon,
Object[] options, Object initialValue)
throws HeadlessException {
int result = CLOSED_OPTION;
VOptionPane optionPane = new VOptionPane();
optionPane.setMessage(message);
optionPane.setOptionType(optionType);
optionPane.setMessageType(messageType);
optionPane.setIcon(icon);
optionPane.setOptions(options);
optionPane.setInitialValue(initialValue);
optionPane.setComponentOrientation((parent != null ? parent : getRootFrame()).getComponentOrientation());
JDialog dialog = optionPane.createDialog(parent, title);
optionPane.selectInitialValue();
dialog.setVisible(true);
Object selectedValue = optionPane.getValue();
if (selectedValue != null) {
if (options == null) {
if (selectedValue instanceof Integer) {
result = ((Integer)selectedValue).intValue();
}
} else {
for (int counter = 0, maxCounter = options.length; counter < maxCounter; counter++) {
if (options[counter].equals(selectedValue)) {
result = counter;
break;
}
}
}
}
return result;
}
static Object showWrappingInputDialog(Component parent, Object message, String title,
int messageType, Icon icon,
Object[] selectionValues, Object initialSelectionValue)
throws HeadlessException {
Object result = null;
VOptionPane optionPane = new VOptionPane();
optionPane.setWantsInput(true);
optionPane.setMessage(message);
optionPane.setOptionType(OK_CANCEL_OPTION);
optionPane.setMessageType(messageType);
optionPane.setIcon(icon);
optionPane.setSelectionValues(selectionValues);
optionPane.setInitialSelectionValue(initialSelectionValue);
optionPane.setComponentOrientation((parent != null ? parent : getRootFrame()).getComponentOrientation());
JDialog dialog = optionPane.createDialog(parent, title);
optionPane.selectInitialValue();
dialog.setVisible(true);
Object value = optionPane.getInputValue();
if (value != UNINITIALIZED_VALUE) {
result = value;
}
return result;
}
}