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