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