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 { 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 requiresUploadToServer() { 089 return isModified(); 090 } 091 092 @Override 093 public boolean isSavable() { 094 return true; 095 } 096 097 @Override 098 public boolean requiresSaveToFile() { 099 return getAssociatedFile() != null && isModified(); 100 } 101 102 @Override 103 public void paint(Graphics2D g, MapView mv, Bounds box) { 104 for (Note note : noteData.getNotes()) { 105 Point p = mv.getPoint(note.getLatLon()); 106 107 ImageIcon icon = null; 108 if (note.getId() < 0) { 109 icon = NotesDialog.ICON_NEW_SMALL; 110 } else if (note.getState() == State.closed) { 111 icon = NotesDialog.ICON_CLOSED_SMALL; 112 } else { 113 icon = NotesDialog.ICON_OPEN_SMALL; 114 } 115 int width = icon.getIconWidth(); 116 int height = icon.getIconHeight(); 117 g.drawImage(icon.getImage(), p.x - (width / 2), p.y - height, Main.map.mapView); 118 } 119 if (noteData.getSelectedNote() != null) { 120 StringBuilder sb = new StringBuilder("<html>"); 121 sb.append(tr("Note")) 122 .append(' ').append(noteData.getSelectedNote().getId()); 123 for (NoteComment comment : noteData.getSelectedNote().getComments()) { 124 String commentText = comment.getText(); 125 //closing a note creates an empty comment that we don't want to show 126 if (commentText != null && !commentText.trim().isEmpty()) { 127 sb.append("<hr/>"); 128 String userName = XmlWriter.encode(comment.getUser().getName()); 129 if (userName == null || userName.trim().isEmpty()) { 130 userName = "<Anonymous>"; 131 } 132 sb.append(userName); 133 sb.append(" on "); 134 sb.append(DateUtils.getDateFormat(DateFormat.MEDIUM).format(comment.getCommentTimestamp())); 135 sb.append(":<br/>"); 136 String htmlText = XmlWriter.encode(comment.getText(), true); 137 htmlText = htmlText.replace("
", "<br/>"); //encode method leaves us with entity instead of \n 138 htmlText = htmlText.replace("/", "/\u200b"); //zero width space to wrap long URLs (see #10864) 139 sb.append(htmlText); 140 } 141 } 142 sb.append("</html>"); 143 JToolTip toolTip = new JToolTip(); 144 toolTip.setTipText(sb.toString()); 145 Point p = mv.getPoint(noteData.getSelectedNote().getLatLon()); 146 147 g.setColor(ColorHelper.html2color(Main.pref.get("color.selected"))); 148 g.drawRect(p.x - (NotesDialog.ICON_SMALL_SIZE / 2), p.y - NotesDialog.ICON_SMALL_SIZE, 149 NotesDialog.ICON_SMALL_SIZE - 1, NotesDialog.ICON_SMALL_SIZE - 1); 150 151 int tx = p.x + (NotesDialog.ICON_SMALL_SIZE / 2) + 5; 152 int ty = p.y - NotesDialog.ICON_SMALL_SIZE - 1; 153 g.translate(tx, ty); 154 155 //Carried over from the OSB plugin. Not entirely sure why it is needed 156 //but without it, the tooltip doesn't get sized correctly 157 for (int x = 0; x < 2; x++) { 158 Dimension d = toolTip.getUI().getPreferredSize(toolTip); 159 d.width = Math.min(d.width, mv.getWidth() / 2); 160 if (d.width > 0 && d.height > 0) { 161 toolTip.setSize(d); 162 try { 163 toolTip.paint(g); 164 } catch (IllegalArgumentException e) { 165 // See #11123 - https://bugs.openjdk.java.net/browse/JDK-6719550 166 // Ignore the exception, as Netbeans does: http://hg.netbeans.org/main-silver/rev/c96f4d5fbd20 167 Main.error(e, false); 168 } 169 } 170 } 171 g.translate(-tx, -ty); 172 } 173 } 174 175 @Override 176 public Icon getIcon() { 177 return NotesDialog.ICON_OPEN_SMALL; 178 } 179 180 @Override 181 public String getToolTipText() { 182 return noteData.getNotes().size() + ' ' + tr("Notes"); 183 } 184 185 @Override 186 public void mergeFrom(Layer from) { 187 throw new UnsupportedOperationException("Notes layer does not support merging yet"); 188 } 189 190 @Override 191 public boolean isMergable(Layer other) { 192 return false; 193 } 194 195 @Override 196 public void visitBoundingBox(BoundingXYVisitor v) { 197 for (Note note : noteData.getNotes()) { 198 v.visit(note.getLatLon()); 199 } 200 } 201 202 @Override 203 public Object getInfoComponent() { 204 StringBuilder sb = new StringBuilder(); 205 sb.append(tr("Notes layer")) 206 .append('\n') 207 .append(tr("Total notes:")) 208 .append(' ') 209 .append(noteData.getNotes().size()) 210 .append('\n') 211 .append(tr("Changes need uploading?")) 212 .append(' ') 213 .append(isModified()); 214 return sb.toString(); 215 } 216 217 @Override 218 public Action[] getMenuEntries() { 219 List<Action> actions = new ArrayList<>(); 220 actions.add(LayerListDialog.getInstance().createShowHideLayerAction()); 221 actions.add(LayerListDialog.getInstance().createDeleteLayerAction()); 222 actions.add(new LayerListPopup.InfoAction(this)); 223 actions.add(new LayerSaveAction(this)); 224 actions.add(new LayerSaveAsAction(this)); 225 return actions.toArray(new Action[actions.size()]); 226 } 227 228 @Override 229 public void mouseClicked(MouseEvent e) { 230 if (SwingUtilities.isRightMouseButton(e) && noteData.getSelectedNote() != null) { 231 final String url = OsmApi.getOsmApi().getBaseUrl() + "notes/" + noteData.getSelectedNote().getId(); 232 Utils.copyToClipboard(url); 233 return; 234 } else if (!SwingUtilities.isLeftMouseButton(e)) { 235 return; 236 } 237 Point clickPoint = e.getPoint(); 238 double snapDistance = 10; 239 double minDistance = Double.MAX_VALUE; 240 Note closestNote = null; 241 for (Note note : noteData.getNotes()) { 242 Point notePoint = Main.map.mapView.getPoint(note.getLatLon()); 243 //move the note point to the center of the icon where users are most likely to click when selecting 244 notePoint.setLocation(notePoint.getX(), notePoint.getY() - NotesDialog.ICON_SMALL_SIZE / 2); 245 double dist = clickPoint.distanceSq(notePoint); 246 if (minDistance > dist && clickPoint.distance(notePoint) < snapDistance) { 247 minDistance = dist; 248 closestNote = note; 249 } 250 } 251 noteData.setSelectedNote(closestNote); 252 } 253 254 @Override 255 public File createAndOpenSaveFileChooser() { 256 return SaveActionBase.createAndOpenSaveFileChooser(tr("Save GPX file"), NoteExporter.FILE_FILTER); 257 } 258 259 @Override 260 public AbstractIOTask createUploadTask(ProgressMonitor monitor) { 261 return new UploadNoteLayerTask(this, monitor); 262 } 263 264 @Override 265 public void mousePressed(MouseEvent e) { 266 // Do nothing 267 } 268 269 @Override 270 public void mouseReleased(MouseEvent e) { 271 // Do nothing 272 } 273 274 @Override 275 public void mouseEntered(MouseEvent e) { 276 // Do nothing 277 } 278 279 @Override 280 public void mouseExited(MouseEvent e) { 281 // Do nothing 282 } 283}