components/visual-panels/core/src/java/vpanels/panel/com/oracle/solaris/vp/panel/swing/timezone/TimeZonePanel.java
changeset 827 0944d8c0158b
child 1410 ca9946e5736c
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/visual-panels/core/src/java/vpanels/panel/com/oracle/solaris/vp/panel/swing/timezone/TimeZonePanel.java	Thu May 24 04:16:47 2012 -0400
@@ -0,0 +1,390 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License (the "License").
+ * You may not use this file except in compliance with the License.
+ *
+ * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
+ * or http://www.opensolaris.org/os/licensing.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
+ * If applicable, add the following below this CDDL HEADER, with the
+ * fields enclosed by brackets "[]" replaced with your own identifying
+ * information: Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ */
+
+/*
+ * Copyright (c) 2010, 2012, Oracle and/or its affiliates. All rights reserved.
+ */
+
+package com.oracle.solaris.vp.panel.swing.timezone;
+
+import java.awt.*;
+import java.awt.geom.Point2D;
+import java.beans.*;
+import java.text.DateFormat;
+import java.util.*;
+import java.util.List;
+import javax.swing.event.*;
+import javax.swing.Icon;
+import com.oracle.solaris.vp.panel.common.model.View;
+import com.oracle.solaris.vp.panels.time.*;
+import com.oracle.solaris.vp.util.misc.finder.Finder;
+import com.oracle.solaris.vp.util.misc.property.MutableProperty;
+import com.oracle.solaris.vp.util.swing.layout.ColumnLayoutConstraint;
+import com.oracle.solaris.vp.util.swing.SettingsPanel;
+import com.oracle.solaris.vp.util.swing.time.*;
+
+public class TimeZonePanel extends SettingsPanel
+    implements View<TimeZoneModel> {
+
+    //
+    // Static data
+    //
+
+    private static TimeZoneMapper MAPPER = new TimeZoneMapper();
+
+    private static final Icon BACKGROUND_ICON =
+	Finder.getIcon("images/world_map-960.png");
+
+    /**
+     * Gets the {@code TimeZone} that corresponds to the given {@link
+     * TimeZoneInfo}.
+     *
+     * @return	a {@code TimeZone}, or {@code null} if no {@code
+     *		TimeZone} corresponds to the given {@link TimeZoneInfo}
+     */
+    public static TimeZone toTimeZone(TimeZoneInfo info) {
+	// Unfortunately TimeZone.getTimeZone(String id) gives no error when
+	// id is not recognized, so use a different approach
+	String[] ids = TimeZone.getAvailableIDs();
+	for (String id : ids) {
+	    if (id.equals(info.getName()) ||
+		id.equals(info.getAltName())) {
+		return TimeZone.getTimeZone(id);
+	    }
+	}
+
+	// XXX We should never return null.  TimeZoneInfo should have a
+	// int getRawOffset method to ensure we can always create a
+	// TimeZone.
+	return null;
+    }
+
+    //
+    // Instance data
+    //
+
+    private LocationPanel<TZChoice> locationPanel_;
+    private MutableProperty<TZChoice> locationProp_;
+
+    private Map<String, TZChoice> nameToTimeZone =
+	new HashMap<String, TZChoice>();
+    private Map<String, Continent> continentDesc =
+	new HashMap<String, Continent>();
+    private Map<String, Country> countryDesc = new HashMap<String, Country>();
+    private Map<Integer, Offset> offsetDesc = new HashMap<Integer, Offset>();
+    private Map<Integer, Offset> rawoffsetDesc = new HashMap<Integer, Offset>();
+
+    //
+    // Inner classes
+    //
+
+    /*
+     * Need an object to encapsulate a timezone offset.
+     */
+    private class Offset {
+	int offset_;
+
+	public Offset(int offset) {
+	    offset_ = offset;
+	}
+
+	@Override
+	public String toString() {
+	    boolean neg = offset_ < 0;
+	    int minutes = Math.abs(offset_) / (60 * 1000);
+	    int hours = minutes / 60;
+	    minutes = minutes % 60;
+	    return (String.format("GMT%s%d:%02d",
+		neg ? "-" : "+", hours, minutes));
+	}
+    }
+
+    /*
+     * We could use TimeZoneInfos directly, but instead we use this wrapper
+     * type to cache the associations between each time zone and its various
+     * attributes.
+     */
+    private class TZChoice {
+	TimeZoneInfo info_;
+	Country country_;
+	Continent continent_;
+	Offset offset_;
+	Offset rawoffset_;
+	String label_;
+	Point2D point_;
+
+	public TZChoice(TimeZoneInfo info) {
+	    TimeZone tz = TimeZone.getTimeZone(info.getName());
+	    info_ = info;
+	    String contname = info.getName().split("/")[0];
+	    continent_ = continentDesc.get(contname);
+	    country_ = countryDesc.get(info.getCountryCode());
+	    offset_ = makeOffset(tz.getOffset(System.currentTimeMillis()),
+		offsetDesc);
+	    rawoffset_ = makeOffset(tz.getRawOffset(), rawoffsetDesc);
+
+	    String alt = info.getAltName();
+	    String name = alt != null ? alt : info.getName();
+	    String display = tz.getDisplayName();
+	    label_ = Finder.getString("timezone.description", name, display);
+
+	    Coordinates coords = info.getCoordinates();
+	    double east = (double)coords.getDegreesE() +
+		(coords.getDegreesE() > 0 ? 1 : -1) *
+		((double)coords.getSecondsE() / 60 +
+		(double)coords.getMinutesE()) / 60;
+	    double north = (double)coords.getDegreesN() +
+		(coords.getDegreesN() > 0 ? 1 : -1) *
+		((double)coords.getSecondsN() / 60 +
+		(double)coords.getMinutesN()) / 60;
+	    east = 180 + east;
+	    north = 90 - north;
+	    point_ = new Point2D.Double(east / 360, north / 180);
+	}
+
+	private Offset makeOffset(int offset, Map<Integer, Offset>map) {
+	    Offset result = map.get(offset);
+	    if (result == null)
+		map.put(offset, result = new Offset(offset));
+	    return (result);
+	}
+
+	public TimeZoneInfo getInfo() {
+	    return (info_);
+	}
+    }
+
+    private static class TimeZoneMapper implements LocationMapper<TZChoice> {
+	@Override
+	public Point2D map(TZChoice choice) {
+	    return choice.point_;
+	}
+
+	@Override
+	public String getLabel(TZChoice choice) {
+	    return choice.label_;
+	}
+    }
+
+    private class TZOffsetCriteria extends LocationCriteria<TZChoice, Offset>
+	implements Comparator<Offset> {
+
+	public String getLabel() {
+	    return Finder.getString("timezone.label.offset");
+	}
+
+	public String getUnselectedText() {
+	    return Finder.getString("timezone.offset.select");
+	}
+
+	@Override
+	public Comparator<Offset> getComparator() {
+	    return this;
+	}
+
+	public Offset toCriterion(TZChoice choice) {
+	    return choice.offset_;
+	}
+
+	public String toDescription(Offset criterion) {
+	    return criterion.toString();
+	}
+
+	/*
+	 * Comparator methods
+	 */
+
+	public int compare(Offset o1, Offset o2) {
+	    return (o1.offset_ - o2.offset_);
+	}
+    }
+
+    private class TZContinentCriteria
+	extends LocationCriteria<TZChoice, Continent> {
+
+	public String getLabel() {
+	    return Finder.getString("timezone.label.continent");
+	}
+
+	public String getUnselectedText() {
+	    return Finder.getString("timezone.continent.select");
+	}
+
+	public Continent toCriterion(TZChoice choice) {
+	    return (choice.continent_);
+	}
+
+	public String toDescription(Continent criterion) {
+	    return criterion.getDescription();
+	}
+    }
+
+    private class TZCountryCriteria
+	extends LocationCriteria<TZChoice, Country> {
+
+	public String getLabel() {
+	    return Finder.getString("timezone.label.country");
+	}
+
+	public String getUnselectedText() {
+	    return Finder.getString("timezone.country.select");
+	}
+
+	public Country toCriterion(TZChoice choice) {
+	    return (choice.country_);
+	}
+
+	public String toDescription(Country criterion) {
+	    return criterion.getDescription();
+	}
+    }
+
+    private class TZZoneCriteria extends LocationCriteria<TZChoice, TZChoice> {
+
+	public String getLabel() {
+	    return Finder.getString("timezone.label.timezone");
+	}
+
+	public String getUnselectedText() {
+	    return Finder.getString("timezone.timezone.select");
+	}
+
+	public TZChoice toCriterion(TZChoice choice) {
+	    return choice;
+	}
+
+	public String toDescription(TZChoice criterion) {
+	    return (MAPPER.getLabel(criterion));
+	}
+    }
+
+    //
+    // Constructors
+    //
+
+    public TimeZonePanel(TimeMXBean bean, final SimpleTimeModel model) {
+
+	getHelpField().setText(Finder.getString("timezone.desc"));
+
+	// Remove the spacing between the help and content panes
+	ColumnLayoutConstraint constraint =
+	    getLayout().getConstraint(getContentPane());
+	constraint.setGap(0);
+
+	/*
+	 * Read metadata from MBean.  Ideally would be read from the model.
+	 */
+	List<TimeZoneInfo> infoSet = bean.gettimeZones();
+	List<Continent> continents = bean.getcontinents();
+	List<Country> countries = bean.getcountries();
+
+	for (Continent c : continents)
+	    continentDesc.put(c.getName(), c);
+
+	for (Country c : countries)
+	    countryDesc.put(c.getCode(), c);
+
+	List<TZChoice> choices = new LinkedList<TZChoice>();
+	for (TimeZoneInfo zone : infoSet) {
+	    TZChoice choice = new TZChoice(zone);
+	    if (choice.continent_ == null || choice.country_ == null)
+		continue;
+	    choices.add(choice);
+	    nameToTimeZone.put(zone.getName(), choice);
+	    if (zone.getAltName() != null)
+		nameToTimeZone.put(zone.getAltName(), choice);
+	}
+
+	List<LocationCriteria<TZChoice, ?>> criteria =
+	    new LinkedList<LocationCriteria<TZChoice, ?>>();
+	criteria.add(new TZContinentCriteria());
+	criteria.add(new TZCountryCriteria());
+	// criteria.add(new TZOffsetCriteria());  // *instead* of the other two
+
+	locationPanel_ = new LocationPanel<TZChoice>(BACKGROUND_ICON,
+	    new Dimension(400, 200), MAPPER);
+	locationProp_ = locationPanel_.getLocationProperty();
+	getChangeableAggregator().addChangeables(locationProp_);
+	locationPanel_.setCriteria(criteria, new TZZoneCriteria(), choices,
+	    model != null ? Finder.getString("timezone.label.time") : null);
+
+	if (model != null) {
+	    /*
+	     * Add listener that updates SimpleTimeModel with selected
+	     * time zone
+	     */
+	    locationProp_.addChangeListener(
+		new ChangeListener() {
+		    @Override
+		    public void stateChanged(ChangeEvent e) {
+			TZChoice choice = locationProp_.getValue();
+			if (choice != null) {
+			    TimeZone zone = toTimeZone(choice.getInfo());
+			    model.setTimeZone(zone);
+			}
+		    }
+		});
+
+	    /*
+	     * And add a listener to the model that updates the auxiliary
+	     * field of the location panel with the current time.
+	     */
+	    final DateFormat format = DateFormat.getDateTimeInstance(
+		DateFormat.SHORT, DateFormat.SHORT);
+	    format.setCalendar(model.getModelCalendar());
+
+	    PropertyChangeListener timeLabelListener =
+		new PropertyChangeListener() {
+		    @Override
+		    public void propertyChange(PropertyChangeEvent e) {
+			locationPanel_.getAuxField().setText(format.format(
+			    model.getCalendar().getTime()));
+		    }
+		};
+	    model.addPropertyChangeListener(TimeModel.PROPERTY_TIME,
+		timeLabelListener);
+
+	    // Initialize
+	    timeLabelListener.propertyChange(null);
+	}
+
+	getContentPane().add(locationPanel_);
+	setContent(locationPanel_, false, false);
+    }
+
+    //
+    // View methods
+    //
+
+    public void modelToView(TimeZoneModel model) {
+	// Sanity check -- the UI should be updated only on the event thread
+	assert EventQueue.isDispatchThread();
+
+	String name = model.getTimeZone();
+	TZChoice timeZone = nameToTimeZone.get(name);
+	locationProp_.update(timeZone, false);
+    }
+
+    public void viewToModel(TimeZoneModel model) {
+	TZChoice tz = locationProp_.getValue();
+	model.setTimeZone(tz != null ? tz.getInfo().getName() : null);
+    }
+}