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