001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.history; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005import static org.openstreetmap.josm.tools.I18n.trn; 006 007import java.awt.BorderLayout; 008import java.awt.Dimension; 009import java.awt.GridBagConstraints; 010import java.awt.GridBagLayout; 011import java.awt.GridLayout; 012import java.awt.Insets; 013import java.awt.event.ActionEvent; 014import java.text.DateFormat; 015import java.util.Collections; 016import java.util.Date; 017import java.util.Observable; 018import java.util.Observer; 019 020import javax.swing.AbstractAction; 021import javax.swing.JButton; 022import javax.swing.JComponent; 023import javax.swing.JLabel; 024import javax.swing.JPanel; 025import javax.swing.JTextArea; 026 027import org.openstreetmap.josm.Main; 028import org.openstreetmap.josm.data.osm.Changeset; 029import org.openstreetmap.josm.data.osm.OsmPrimitive; 030import org.openstreetmap.josm.data.osm.User; 031import org.openstreetmap.josm.data.osm.history.HistoryOsmPrimitive; 032import org.openstreetmap.josm.gui.JosmUserIdentityManager; 033import org.openstreetmap.josm.gui.dialogs.ChangesetDialog; 034import org.openstreetmap.josm.gui.dialogs.changeset.ChangesetCacheManager; 035import org.openstreetmap.josm.gui.dialogs.changeset.ChangesetDiscussionPanel; 036import org.openstreetmap.josm.gui.layer.OsmDataLayer; 037import org.openstreetmap.josm.gui.widgets.JMultilineLabel; 038import org.openstreetmap.josm.gui.widgets.UrlLabel; 039import org.openstreetmap.josm.tools.CheckParameterUtil; 040import org.openstreetmap.josm.tools.GBC; 041import org.openstreetmap.josm.tools.ImageProvider; 042import org.openstreetmap.josm.tools.Utils; 043import org.openstreetmap.josm.tools.date.DateUtils; 044 045/** 046 * VersionInfoPanel is an UI component which displays the basic properties of a version 047 * of a {@link OsmPrimitive}. 048 * @since 1709 049 */ 050public class VersionInfoPanel extends JPanel implements Observer { 051 private final PointInTimeType pointInTimeType; 052 private final transient HistoryBrowserModel model; 053 private JMultilineLabel lblInfo; 054 private UrlLabel lblUser; 055 private UrlLabel lblChangeset; 056 private final JButton lblChangesetComments = new JButton(ImageProvider.get("dialogs/notes/note_comment")); 057 private final OpenChangesetDialogAction changesetCommentsDialogAction = new OpenChangesetDialogAction(ChangesetDiscussionPanel.class); 058 private final OpenChangesetDialogAction changesetDialogAction = new OpenChangesetDialogAction(null); 059 private final JButton changesetButton = new JButton(changesetDialogAction); 060 private JPanel pnlChangesetSource; 061 private JPanel pnlChangesetImageryUsed; 062 private JLabel lblSource; 063 private JLabel lblImageryUsed; 064 private JTextArea texChangesetComment; 065 private JTextArea texChangesetSource; 066 private JTextArea texChangesetImageryUsed; 067 068 protected static JTextArea buildTextArea(String tooltip) { 069 JTextArea lbl = new JTextArea(); 070 lbl.setLineWrap(true); 071 lbl.setWrapStyleWord(true); 072 lbl.setEditable(false); 073 lbl.setOpaque(false); 074 lbl.setToolTipText(tooltip); 075 return lbl; 076 } 077 078 protected static JLabel buildLabel(String text, String tooltip, JTextArea textArea) { 079 // We need text field to be a JTextArea for line wrapping but cannot put HTML code in here, 080 // so create a separate JLabel with same characteristics (margin, font) 081 JLabel lbl = new JLabel("<html><p style='margin-top:"+textArea.getMargin().top+"'>"+text+"</html>"); 082 lbl.setFont(textArea.getFont()); 083 lbl.setToolTipText(tooltip); 084 lbl.setLabelFor(textArea); 085 return lbl; 086 } 087 088 protected static JPanel buildTextPanel(JLabel label, JTextArea textArea) { 089 JPanel pnl = new JPanel(new GridBagLayout()); 090 pnl.add(label, GBC.std().anchor(GBC.NORTHWEST)); 091 pnl.add(textArea, GBC.eol().insets(2, 0, 0, 0).fill()); 092 return pnl; 093 } 094 095 protected void build() { 096 JPanel pnl1 = new JPanel(new BorderLayout()); 097 lblInfo = new JMultilineLabel(""); 098 pnl1.add(lblInfo, BorderLayout.CENTER); 099 100 JPanel pnlUserAndChangeset = new JPanel(new GridLayout(2, 2)); 101 lblUser = new UrlLabel("", 2); 102 pnlUserAndChangeset.add(new JLabel(tr("User:"))); 103 pnlUserAndChangeset.add(lblUser); 104 changesetButton.setMargin(new Insets(0, 0, 0, 0)); 105 pnlUserAndChangeset.add(changesetButton); 106 lblChangeset = new UrlLabel("", 2); 107 final JPanel pnlChangesetInfo = new JPanel(new BorderLayout()); 108 pnlChangesetInfo.add(lblChangeset, BorderLayout.CENTER); 109 lblChangesetComments.setAction(changesetCommentsDialogAction); 110 lblChangesetComments.setMargin(new Insets(0, 0, 0, 0)); 111 lblChangesetComments.setIcon(new ImageProvider("dialogs/notes/note_comment").setMaxSize(12).get()); 112 pnlChangesetInfo.add(lblChangesetComments, BorderLayout.EAST); 113 pnlUserAndChangeset.add(pnlChangesetInfo); 114 115 texChangesetComment = buildTextArea(tr("Changeset comment")); 116 texChangesetSource = buildTextArea(tr("Changeset source")); 117 texChangesetImageryUsed = buildTextArea(tr("Imagery used")); 118 119 lblSource = buildLabel(tr("<b>Source</b>:"), tr("Changeset source"), texChangesetSource); 120 lblImageryUsed = buildLabel(tr("<b>Imagery</b>:"), tr("Imagery used"), texChangesetImageryUsed); 121 pnlChangesetSource = buildTextPanel(lblSource, texChangesetSource); 122 pnlChangesetImageryUsed = buildTextPanel(lblImageryUsed, texChangesetImageryUsed); 123 124 setLayout(new GridBagLayout()); 125 GridBagConstraints gc = new GridBagConstraints(); 126 gc.anchor = GridBagConstraints.NORTHWEST; 127 gc.fill = GridBagConstraints.HORIZONTAL; 128 gc.weightx = 1.0; 129 gc.weighty = 1.0; 130 add(pnl1, gc); 131 gc.gridy = 1; 132 gc.weighty = 0.0; 133 add(pnlUserAndChangeset, gc); 134 gc.gridy = 2; 135 add(texChangesetComment, gc); 136 gc.gridy = 3; 137 add(pnlChangesetSource, gc); 138 gc.gridy = 4; 139 add(pnlChangesetImageryUsed, gc); 140 } 141 142 protected HistoryOsmPrimitive getPrimitive() { 143 if (model == null || pointInTimeType == null) 144 return null; 145 return model.getPointInTime(pointInTimeType); 146 } 147 148 protected String getInfoText(final Date timestamp, final long version, final boolean isLatest) { 149 String text; 150 if (isLatest) { 151 OsmDataLayer editLayer = Main.main.getEditLayer(); 152 text = tr("<html>Version <strong>{0}</strong> currently edited in layer ''{1}''</html>", 153 Long.toString(version), 154 editLayer == null ? tr("unknown") : editLayer.getName() 155 ); 156 } else { 157 String date = "?"; 158 if (timestamp != null) { 159 date = DateUtils.formatDateTime(timestamp, DateFormat.SHORT, DateFormat.SHORT); 160 } 161 text = tr( 162 "<html>Version <strong>{0}</strong> created on <strong>{1}</strong></html>", 163 Long.toString(version), date); 164 } 165 return text; 166 } 167 168 /** 169 * Constructs a new {@code VersionInfoPanel}. 170 */ 171 public VersionInfoPanel() { 172 pointInTimeType = null; 173 model = null; 174 build(); 175 } 176 177 /** 178 * constructor 179 * 180 * @param model the model (must not be null) 181 * @param pointInTimeType the point in time this panel visualizes (must not be null) 182 * @throws IllegalArgumentException if model is null 183 * @throws IllegalArgumentException if pointInTimeType is null 184 */ 185 public VersionInfoPanel(HistoryBrowserModel model, PointInTimeType pointInTimeType) { 186 CheckParameterUtil.ensureParameterNotNull(pointInTimeType, "pointInTimeType"); 187 CheckParameterUtil.ensureParameterNotNull(model, "model"); 188 189 this.model = model; 190 this.pointInTimeType = pointInTimeType; 191 model.addObserver(this); 192 build(); 193 } 194 195 protected static String getUserUrl(String username) { 196 return Main.getBaseUserUrl() + '/' + Utils.encodeUrl(username).replaceAll("\\+", "%20"); 197 } 198 199 @Override 200 public void update(Observable o, Object arg) { 201 HistoryOsmPrimitive primitive = getPrimitive(); 202 if (primitive != null) { 203 Changeset cs = primitive.getChangeset(); 204 update(cs, model.isLatest(primitive), primitive.getTimestamp(), primitive.getVersion()); 205 } 206 } 207 208 /** 209 * Updates the content of this panel based on the changeset information given by {@code primitive}. 210 * @param primitive the primitive to extract the changeset information from 211 * @param isLatest whether this relates to a not yet commited changeset 212 */ 213 public void update(final OsmPrimitive primitive, final boolean isLatest) { 214 update(Changeset.fromPrimitive(primitive), isLatest, primitive.getTimestamp(), primitive.getVersion()); 215 } 216 217 /** 218 * Updates the content of this panel based on the changeset information given by {@code cs}. 219 * @param cs the changeset information 220 * @param isLatest whether this relates to a not yet commited changeset 221 * @param timestamp the timestamp 222 * @param version the version of the primitive 223 */ 224 public void update(final Changeset cs, final boolean isLatest, final Date timestamp, final long version) { 225 lblInfo.setText(getInfoText(timestamp, version, isLatest)); 226 227 if (!isLatest) { 228 User user = cs.getUser(); 229 String url = Main.getBaseBrowseUrl() + "/changeset/" + cs.getId(); 230 lblChangeset.setUrl(url); 231 lblChangeset.setDescription(Long.toString(cs.getId())); 232 changesetCommentsDialogAction.setId(cs.getId()); 233 lblChangesetComments.setVisible(cs.getCommentsCount() > 0); 234 lblChangesetComments.setText(String.valueOf(cs.getCommentsCount())); 235 lblChangesetComments.setToolTipText(trn("This changeset has {0} comment", "This changeset has {0} comments", 236 cs.getCommentsCount(), cs.getCommentsCount())); 237 changesetDialogAction.setId(cs.getId()); 238 changesetButton.setEnabled(true); 239 240 String username = ""; 241 if (user != null) { 242 username = user.getName(); 243 } 244 lblUser.setDescription(username); 245 if (user != null && user != User.getAnonymous()) { 246 lblUser.setUrl(getUserUrl(username)); 247 } else { 248 lblUser.setUrl(null); 249 } 250 } else { 251 String username = JosmUserIdentityManager.getInstance().getUserName(); 252 if (username == null) { 253 lblUser.setDescription(tr("anonymous")); 254 lblUser.setUrl(null); 255 } else { 256 lblUser.setDescription(username); 257 lblUser.setUrl(getUserUrl(username)); 258 } 259 lblChangeset.setDescription(tr("none")); 260 lblChangeset.setUrl(null); 261 lblChangesetComments.setVisible(false); 262 changesetDialogAction.setId(null); 263 changesetButton.setEnabled(false); 264 } 265 266 final Changeset oppCs = model != null ? model.getPointInTime(pointInTimeType.opposite()).getChangeset() : null; 267 updateText(cs, "comment", texChangesetComment, null, oppCs, texChangesetComment); 268 updateText(cs, "source", texChangesetSource, lblSource, oppCs, pnlChangesetSource); 269 updateText(cs, "imagery_used", texChangesetImageryUsed, lblImageryUsed, oppCs, pnlChangesetImageryUsed); 270 } 271 272 protected static void updateText(Changeset cs, String attr, JTextArea textArea, JLabel label, Changeset oppCs, JComponent container) { 273 final String text = cs != null ? cs.get(attr) : null; 274 // Update text, hide prefixing label if empty 275 if (label != null) { 276 label.setVisible(text != null && !Utils.strip(text).isEmpty()); 277 } 278 textArea.setText(text); 279 // Hide container if values of both versions are empty 280 container.setVisible(text != null || (oppCs != null && oppCs.get(attr) != null)); 281 } 282 283 static class OpenChangesetDialogAction extends AbstractAction { 284 private final Class<? extends JComponent> componentToSelect; 285 private Integer id; 286 287 OpenChangesetDialogAction(Class<? extends JComponent> componentToSelect) { 288 super(tr("Changeset"), new ImageProvider("dialogs/changeset", "changesetmanager").resetMaxSize(new Dimension(16, 16)).get()); 289 putValue(SHORT_DESCRIPTION, tr("Opens the Changeset Manager window for the selected changesets")); 290 this.componentToSelect = componentToSelect; 291 } 292 293 void setId(Integer id) { 294 this.id = id; 295 } 296 297 @Override 298 public void actionPerformed(ActionEvent e) { 299 ChangesetDialog.LaunchChangesetManager.displayChangesets(Collections.singleton(id)); 300 if (componentToSelect != null) { 301 ChangesetCacheManager.getInstance().setSelectedComponentInDetailPanel(componentToSelect); 302 } 303 } 304 } 305}