001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.layer; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.Dimension; 007import java.awt.Graphics2D; 008import java.awt.Point; 009import java.awt.event.MouseEvent; 010import java.awt.event.MouseListener; 011import java.io.File; 012import java.text.DateFormat; 013import java.util.ArrayList; 014import java.util.Collection; 015import java.util.Collections; 016import java.util.List; 017 018import javax.swing.Action; 019import javax.swing.Icon; 020import javax.swing.ImageIcon; 021import javax.swing.JToolTip; 022import javax.swing.SwingUtilities; 023 024import org.openstreetmap.josm.Main; 025import org.openstreetmap.josm.actions.SaveActionBase; 026import org.openstreetmap.josm.data.Bounds; 027import org.openstreetmap.josm.data.notes.Note; 028import org.openstreetmap.josm.data.notes.Note.State; 029import org.openstreetmap.josm.data.notes.NoteComment; 030import org.openstreetmap.josm.data.osm.NoteData; 031import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor; 032import org.openstreetmap.josm.gui.MapView; 033import org.openstreetmap.josm.gui.dialogs.LayerListDialog; 034import org.openstreetmap.josm.gui.dialogs.LayerListPopup; 035import org.openstreetmap.josm.gui.dialogs.NotesDialog; 036import org.openstreetmap.josm.gui.io.AbstractIOTask; 037import org.openstreetmap.josm.gui.io.UploadNoteLayerTask; 038import org.openstreetmap.josm.gui.progress.ProgressMonitor; 039import org.openstreetmap.josm.io.NoteExporter; 040import org.openstreetmap.josm.io.OsmApi; 041import org.openstreetmap.josm.io.XmlWriter; 042import org.openstreetmap.josm.tools.ColorHelper; 043import org.openstreetmap.josm.tools.Utils; 044import org.openstreetmap.josm.tools.date.DateUtils; 045 046/** 047 * A layer to hold Note objects. 048 * @since 7522 049 */ 050public class NoteLayer extends AbstractModifiableLayer implements MouseListener, UploadToServer, SaveToFile { 051 052 private final NoteData noteData; 053 054 /** 055 * Create a new note layer with a set of notes 056 * @param notes A list of notes to show in this layer 057 * @param name The name of the layer. Typically "Notes" 058 */ 059 public NoteLayer(Collection<Note> notes, String name) { 060 super(name); 061 noteData = new NoteData(notes); 062 } 063 064 /** Convenience constructor that creates a layer with an empty note list */ 065 public NoteLayer() { 066 this(Collections.<Note>emptySet(), tr("Notes")); 067 } 068 069 @Override 070 public void hookUpMapView() { 071 Main.map.mapView.addMouseListener(this); 072 } 073 074 /** 075 * Returns the note data store being used by this layer 076 * @return noteData containing layer notes 077 */ 078 public NoteData getNoteData() { 079 return noteData; 080 } 081 082 @Override 083 public boolean isModified() { 084 return noteData.isModified(); 085 } 086 087 @Override 088 public boolean isUploadable() { 089 return true; 090 } 091 092 @Override 093 public boolean requiresUploadToServer() { 094 return isModified(); 095 } 096 097 @Override 098 public boolean isSavable() { 099 return true; 100 } 101 102 @Override 103 public boolean requiresSaveToFile() { 104 return getAssociatedFile() != null && isModified(); 105 } 106 107 @Override 108 public void paint(Graphics2D g, MapView mv, Bounds box) { 109 for (Note note : noteData.getNotes()) { 110 Point p = mv.getPoint(note.getLatLon()); 111 112 ImageIcon icon = null; 113 if (note.getId() < 0) { 114 icon = NotesDialog.ICON_NEW_SMALL; 115 } else if (note.getState() == State.closed) { 116 icon = NotesDialog.ICON_CLOSED_SMALL; 117 } else { 118 icon = NotesDialog.ICON_OPEN_SMALL; 119 } 120 int width = icon.getIconWidth(); 121 int height = icon.getIconHeight(); 122 g.drawImage(icon.getImage(), p.x - (width / 2), p.y - height, Main.map.mapView); 123 } 124 if (noteData.getSelectedNote() != null) { 125 StringBuilder sb = new StringBuilder("<html>"); 126 sb.append(tr("Note")) 127 .append(' ').append(noteData.getSelectedNote().getId()); 128 for (NoteComment comment : noteData.getSelectedNote().getComments()) { 129 String commentText = comment.getText(); 130 //closing a note creates an empty comment that we don't want to show 131 if (commentText != null && !commentText.trim().isEmpty()) { 132 sb.append("<hr/>"); 133 String userName = XmlWriter.encode(comment.getUser().getName()); 134 if (userName == null || userName.trim().isEmpty()) { 135 userName = "<Anonymous>"; 136 } 137 sb.append(userName); 138 sb.append(" on "); 139 sb.append(DateUtils.getDateFormat(DateFormat.MEDIUM).format(comment.getCommentTimestamp())); 140 sb.append(":<br/>"); 141 String htmlText = XmlWriter.encode(comment.getText(), true); 142 htmlText = htmlText.replace("
", "<br/>"); //encode method leaves us with entity instead of \n 143 htmlText = htmlText.replace("/", "/\u200b"); //zero width space to wrap long URLs (see #10864) 144 sb.append(htmlText); 145 } 146 } 147 sb.append("</html>"); 148 JToolTip toolTip = new JToolTip(); 149 toolTip.setTipText(sb.toString()); 150 Point p = mv.getPoint(noteData.getSelectedNote().getLatLon()); 151 152 g.setColor(ColorHelper.html2color(Main.pref.get("color.selected"))); 153 g.drawRect(p.x - (NotesDialog.ICON_SMALL_SIZE / 2), p.y - NotesDialog.ICON_SMALL_SIZE, 154 NotesDialog.ICON_SMALL_SIZE - 1, NotesDialog.ICON_SMALL_SIZE - 1); 155 156 int tx = p.x + (NotesDialog.ICON_SMALL_SIZE / 2) + 5; 157 int ty = p.y - NotesDialog.ICON_SMALL_SIZE - 1; 158 g.translate(tx, ty); 159 160 //Carried over from the OSB plugin. Not entirely sure why it is needed 161 //but without it, the tooltip doesn't get sized correctly 162 for (int x = 0; x < 2; x++) { 163 Dimension d = toolTip.getUI().getPreferredSize(toolTip); 164 d.width = Math.min(d.width, mv.getWidth() / 2); 165 if (d.width > 0 && d.height > 0) { 166 toolTip.setSize(d); 167 try { 168 toolTip.paint(g); 169 } catch (IllegalArgumentException e) { 170 // See #11123 - https://bugs.openjdk.java.net/browse/JDK-6719550 171 // Ignore the exception, as Netbeans does: http://hg.netbeans.org/main-silver/rev/c96f4d5fbd20 172 Main.error(e, false); 173 } 174 } 175 } 176 g.translate(-tx, -ty); 177 } 178 } 179 180 @Override 181 public Icon getIcon() { 182 return NotesDialog.ICON_OPEN_SMALL; 183 } 184 185 @Override 186 public String getToolTipText() { 187 return noteData.getNotes().size() + ' ' + tr("Notes"); 188 } 189 190 @Override 191 public void mergeFrom(Layer from) { 192 throw new UnsupportedOperationException("Notes layer does not support merging yet"); 193 } 194 195 @Override 196 public boolean isMergable(Layer other) { 197 return false; 198 } 199 200 @Override 201 public void visitBoundingBox(BoundingXYVisitor v) { 202 for (Note note : noteData.getNotes()) { 203 v.visit(note.getLatLon()); 204 } 205 } 206 207 @Override 208 public Object getInfoComponent() { 209 StringBuilder sb = new StringBuilder(); 210 sb.append(tr("Notes layer")) 211 .append('\n') 212 .append(tr("Total notes:")) 213 .append(' ') 214 .append(noteData.getNotes().size()) 215 .append('\n') 216 .append(tr("Changes need uploading?")) 217 .append(' ') 218 .append(isModified()); 219 return sb.toString(); 220 } 221 222 @Override 223 public Action[] getMenuEntries() { 224 List<Action> actions = new ArrayList<>(); 225 actions.add(LayerListDialog.getInstance().createShowHideLayerAction()); 226 actions.add(LayerListDialog.getInstance().createDeleteLayerAction()); 227 actions.add(new LayerListPopup.InfoAction(this)); 228 actions.add(new LayerSaveAction(this)); 229 actions.add(new LayerSaveAsAction(this)); 230 return actions.toArray(new Action[actions.size()]); 231 } 232 233 @Override 234 public void mouseClicked(MouseEvent e) { 235 if (SwingUtilities.isRightMouseButton(e) && noteData.getSelectedNote() != null) { 236 final String url = OsmApi.getOsmApi().getBaseUrl() + "notes/" + noteData.getSelectedNote().getId(); 237 Utils.copyToClipboard(url); 238 return; 239 } else if (!SwingUtilities.isLeftMouseButton(e)) { 240 return; 241 } 242 Point clickPoint = e.getPoint(); 243 double snapDistance = 10; 244 double minDistance = Double.MAX_VALUE; 245 Note closestNote = null; 246 for (Note note : noteData.getNotes()) { 247 Point notePoint = Main.map.mapView.getPoint(note.getLatLon()); 248 //move the note point to the center of the icon where users are most likely to click when selecting 249 notePoint.setLocation(notePoint.getX(), notePoint.getY() - NotesDialog.ICON_SMALL_SIZE / 2); 250 double dist = clickPoint.distanceSq(notePoint); 251 if (minDistance > dist && clickPoint.distance(notePoint) < snapDistance) { 252 minDistance = dist; 253 closestNote = note; 254 } 255 } 256 noteData.setSelectedNote(closestNote); 257 } 258 259 @Override 260 public File createAndOpenSaveFileChooser() { 261 return SaveActionBase.createAndOpenSaveFileChooser(tr("Save GPX file"), NoteExporter.FILE_FILTER); 262 } 263 264 @Override 265 public AbstractIOTask createUploadTask(ProgressMonitor monitor) { 266 return new UploadNoteLayerTask(this, monitor); 267 } 268 269 @Override 270 public void mousePressed(MouseEvent e) { 271 // Do nothing 272 } 273 274 @Override 275 public void mouseReleased(MouseEvent e) { 276 // Do nothing 277 } 278 279 @Override 280 public void mouseEntered(MouseEvent e) { 281 // Do nothing 282 } 283 284 @Override 285 public void mouseExited(MouseEvent e) { 286 // Do nothing 287 } 288}