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 toolTip.setSize(d); 153 toolTip.paint(g); 154 } 155 g.translate(-tx, -ty); 156 } 157 } 158 159 @Override 160 public Icon getIcon() { 161 return NotesDialog.ICON_OPEN_SMALL; 162 } 163 164 @Override 165 public String getToolTipText() { 166 return noteData.getNotes().size() + " " + tr("Notes"); 167 } 168 169 @Override 170 public void mergeFrom(Layer from) { 171 throw new UnsupportedOperationException("Notes layer does not support merging yet"); 172 } 173 174 @Override 175 public boolean isMergable(Layer other) { 176 return false; 177 } 178 179 @Override 180 public void visitBoundingBox(BoundingXYVisitor v) { 181 } 182 183 @Override 184 public Object getInfoComponent() { 185 StringBuilder sb = new StringBuilder(); 186 sb.append(tr("Notes layer")); 187 sb.append("\n"); 188 sb.append(tr("Total notes:")); 189 sb.append(" "); 190 sb.append(noteData.getNotes().size()); 191 sb.append("\n"); 192 sb.append(tr("Changes need uploading?")); 193 sb.append(" "); 194 sb.append(isModified()); 195 return sb.toString(); 196 } 197 198 @Override 199 public Action[] getMenuEntries() { 200 List<Action> actions = new ArrayList<>(); 201 actions.add(LayerListDialog.getInstance().createShowHideLayerAction()); 202 actions.add(LayerListDialog.getInstance().createDeleteLayerAction()); 203 actions.add(new LayerListPopup.InfoAction(this)); 204 actions.add(new LayerSaveAction(this)); 205 actions.add(new LayerSaveAsAction(this)); 206 return actions.toArray(new Action[actions.size()]); 207 } 208 209 @Override 210 public void mouseClicked(MouseEvent e) { 211 if (e.getButton() != MouseEvent.BUTTON1) { 212 return; 213 } 214 Point clickPoint = e.getPoint(); 215 double snapDistance = 10; 216 double minDistance = Double.MAX_VALUE; 217 Note closestNote = null; 218 for (Note note : noteData.getNotes()) { 219 Point notePoint = Main.map.mapView.getPoint(note.getLatLon()); 220 //move the note point to the center of the icon where users are most likely to click when selecting 221 notePoint.setLocation(notePoint.getX(), notePoint.getY() - NotesDialog.ICON_SMALL_SIZE / 2); 222 double dist = clickPoint.distanceSq(notePoint); 223 if (minDistance > dist && clickPoint.distance(notePoint) < snapDistance ) { 224 minDistance = dist; 225 closestNote = note; 226 } 227 } 228 noteData.setSelectedNote(closestNote); 229 } 230 231 @Override 232 public File createAndOpenSaveFileChooser() { 233 return SaveActionBase.createAndOpenSaveFileChooser(tr("Save GPX file"), NoteExporter.FILE_FILTER); 234 } 235 236 @Override 237 public void mousePressed(MouseEvent e) { } 238 239 @Override 240 public void mouseReleased(MouseEvent e) { } 241 242 @Override 243 public void mouseEntered(MouseEvent e) { } 244 245 @Override 246 public void mouseExited(MouseEvent e) { } 247}