source: josm/trunk/src/org/openstreetmap/josm/gui/layer/NoteLayer.java@ 13117

Last change on this file since 13117 was 13117, checked in by Don-vip, 6 years ago

see #11217, fix #15541 - hide note tooltip when selected note is no longer visible + drop old url copy code (does not work anymore with new system)

  • Property svn:eol-style set to native
File size: 12.4 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.layer;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5import static org.openstreetmap.josm.tools.I18n.trn;
6
7import java.awt.Color;
8import java.awt.Dimension;
9import java.awt.Graphics2D;
10import java.awt.Point;
11import java.awt.event.MouseEvent;
12import java.awt.event.MouseListener;
13import java.io.File;
14import java.text.DateFormat;
15import java.util.ArrayList;
16import java.util.Collection;
17import java.util.Collections;
18import java.util.List;
19
20import javax.swing.Action;
21import javax.swing.BorderFactory;
22import javax.swing.Icon;
23import javax.swing.ImageIcon;
24import javax.swing.JWindow;
25import javax.swing.SwingUtilities;
26import javax.swing.UIManager;
27
28import org.openstreetmap.josm.Main;
29import org.openstreetmap.josm.actions.SaveActionBase;
30import org.openstreetmap.josm.data.Bounds;
31import org.openstreetmap.josm.data.notes.Note;
32import org.openstreetmap.josm.data.notes.Note.State;
33import org.openstreetmap.josm.data.notes.NoteComment;
34import org.openstreetmap.josm.data.osm.NoteData;
35import org.openstreetmap.josm.data.osm.NoteData.NoteDataUpdateListener;
36import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
37import org.openstreetmap.josm.gui.MainApplication;
38import org.openstreetmap.josm.gui.MainFrame;
39import org.openstreetmap.josm.gui.MapView;
40import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
41import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
42import org.openstreetmap.josm.gui.io.AbstractIOTask;
43import org.openstreetmap.josm.gui.io.UploadNoteLayerTask;
44import org.openstreetmap.josm.gui.io.importexport.NoteExporter;
45import org.openstreetmap.josm.gui.progress.ProgressMonitor;
46import org.openstreetmap.josm.gui.widgets.HtmlPanel;
47import org.openstreetmap.josm.io.XmlWriter;
48import org.openstreetmap.josm.spi.preferences.Config;
49import org.openstreetmap.josm.tools.ColorHelper;
50import org.openstreetmap.josm.tools.ImageProvider;
51import org.openstreetmap.josm.tools.Logging;
52import org.openstreetmap.josm.tools.date.DateUtils;
53
54/**
55 * A layer to hold Note objects.
56 * @since 7522
57 */
58public class NoteLayer extends AbstractModifiableLayer implements MouseListener, NoteDataUpdateListener {
59
60 private final NoteData noteData;
61
62 private Note displayedNote;
63 private HtmlPanel displayedPanel;
64 private JWindow displayedWindow;
65
66 /**
67 * Create a new note layer with a set of notes
68 * @param notes A list of notes to show in this layer
69 * @param name The name of the layer. Typically "Notes"
70 */
71 public NoteLayer(Collection<Note> notes, String name) {
72 super(name);
73 noteData = new NoteData(notes);
74 noteData.addNoteDataUpdateListener(this);
75 }
76
77 /** Convenience constructor that creates a layer with an empty note list */
78 public NoteLayer() {
79 this(Collections.<Note>emptySet(), tr("Notes"));
80 }
81
82 @Override
83 public void hookUpMapView() {
84 MainApplication.getMap().mapView.addMouseListener(this);
85 }
86
87 @Override
88 public synchronized void destroy() {
89 MainApplication.getMap().mapView.removeMouseListener(this);
90 noteData.removeNoteDataUpdateListener(this);
91 hideNoteWindow();
92 super.destroy();
93 }
94
95 /**
96 * Returns the note data store being used by this layer
97 * @return noteData containing layer notes
98 */
99 public NoteData getNoteData() {
100 return noteData;
101 }
102
103 @Override
104 public boolean isModified() {
105 return noteData.isModified();
106 }
107
108 @Override
109 public boolean isUploadable() {
110 return true;
111 }
112
113 @Override
114 public boolean requiresUploadToServer() {
115 return isModified();
116 }
117
118 @Override
119 public boolean isSavable() {
120 return true;
121 }
122
123 @Override
124 public boolean requiresSaveToFile() {
125 return getAssociatedFile() != null && isModified();
126 }
127
128 @Override
129 public void paint(Graphics2D g, MapView mv, Bounds box) {
130 final int iconHeight = ImageProvider.ImageSizes.SMALLICON.getAdjustedHeight();
131 final int iconWidth = ImageProvider.ImageSizes.SMALLICON.getAdjustedWidth();
132
133 for (Note note : noteData.getNotes()) {
134 Point p = mv.getPoint(note.getLatLon());
135
136 ImageIcon icon;
137 if (note.getId() < 0) {
138 icon = ImageProvider.get("dialogs/notes", "note_new", ImageProvider.ImageSizes.SMALLICON);
139 } else if (note.getState() == State.CLOSED) {
140 icon = ImageProvider.get("dialogs/notes", "note_closed", ImageProvider.ImageSizes.SMALLICON);
141 } else {
142 icon = ImageProvider.get("dialogs/notes", "note_open", ImageProvider.ImageSizes.SMALLICON);
143 }
144 int width = icon.getIconWidth();
145 int height = icon.getIconHeight();
146 g.drawImage(icon.getImage(), p.x - (width / 2), p.y - height, MainApplication.getMap().mapView);
147 }
148 Note selectedNote = noteData.getSelectedNote();
149 if (selectedNote != null) {
150 paintSelectedNote(g, mv, iconHeight, iconWidth, selectedNote);
151 } else {
152 hideNoteWindow();
153 }
154 }
155
156 private void hideNoteWindow() {
157 if (displayedWindow != null) {
158 displayedWindow.setVisible(false);
159 displayedWindow.dispose();
160 displayedWindow = null;
161 displayedPanel = null;
162 displayedNote = null;
163 }
164 }
165
166 private void paintSelectedNote(Graphics2D g, MapView mv, final int iconHeight, final int iconWidth, Note selectedNote) {
167 Point p = mv.getPoint(selectedNote.getLatLon());
168
169 g.setColor(ColorHelper.html2color(Config.getPref().get("color.selected")));
170 g.drawRect(p.x - (iconWidth / 2), p.y - iconHeight, iconWidth - 1, iconHeight - 1);
171
172 if (displayedNote != null && !displayedNote.equals(selectedNote)) {
173 hideNoteWindow();
174 }
175
176 Point screenloc = mv.getLocationOnScreen();
177 int tx = screenloc.x + p.x + (iconWidth / 2) + 5;
178 int ty = screenloc.y + p.y - iconHeight - 1;
179
180 String text = getNoteToolTip(selectedNote);
181
182 if (displayedWindow == null) {
183 displayedPanel = new HtmlPanel(text);
184 displayedPanel.setBackground(UIManager.getColor("ToolTip.background"));
185 displayedPanel.setForeground(UIManager.getColor("ToolTip.foreground"));
186 displayedPanel.setFont(UIManager.getFont("ToolTip.font"));
187 displayedPanel.setBorder(BorderFactory.createLineBorder(Color.black));
188 displayedPanel.enableClickableHyperlinks();
189 fixPanelSize(mv, text);
190 displayedWindow = new JWindow((MainFrame) Main.parent);
191 displayedWindow.add(displayedPanel);
192 } else {
193 displayedPanel.setText(text);
194 fixPanelSize(mv, text);
195 }
196
197 displayedWindow.pack();
198 displayedWindow.setLocation(tx, ty);
199 displayedWindow.setVisible(mv.contains(p));
200 displayedNote = selectedNote;
201 }
202
203 private void fixPanelSize(MapView mv, String text) {
204 Dimension d = displayedPanel.getPreferredSize();
205 if (d.width > mv.getWidth() / 2) {
206 // To make sure long notes such as https://www.openstreetmap.org/note/278197 are displayed correctly
207 displayedPanel.setText(text.replaceAll("\\. ([\\p{Lower}\\p{Upper}\\p{Punct}])", "\\.<br>$1"));
208 }
209 }
210
211 /**
212 * Returns the HTML-formatted tooltip text for the given note.
213 * @param note note to display
214 * @return the HTML-formatted tooltip text for the given note
215 * @since 13111
216 */
217 public static String getNoteToolTip(Note note) {
218 StringBuilder sb = new StringBuilder("<html>");
219 sb.append(tr("Note"))
220 .append(' ').append(note.getId());
221 for (NoteComment comment : note.getComments()) {
222 String commentText = comment.getText();
223 //closing a note creates an empty comment that we don't want to show
224 if (commentText != null && !commentText.trim().isEmpty()) {
225 sb.append("<hr/>");
226 String userName = XmlWriter.encode(comment.getUser().getName());
227 if (userName == null || userName.trim().isEmpty()) {
228 userName = "&lt;Anonymous&gt;";
229 }
230 sb.append(userName)
231 .append(" on ")
232 .append(DateUtils.getDateFormat(DateFormat.MEDIUM).format(comment.getCommentTimestamp()))
233 .append(":<br>");
234 String htmlText = XmlWriter.encode(comment.getText(), true);
235 // encode method leaves us with entity instead of \n
236 htmlText = htmlText.replace("&#xA;", "<br>");
237 // convert URLs to proper HTML links
238 htmlText = htmlText.replaceAll("(https?://\\S+)", "<a href=\"$1\">$1</a>");
239 sb.append(htmlText);
240 }
241 }
242 sb.append("</html>");
243 String result = sb.toString();
244 Logging.debug(result);
245 return result;
246 }
247
248 @Override
249 public Icon getIcon() {
250 return ImageProvider.get("dialogs/notes", "note_open", ImageProvider.ImageSizes.SMALLICON);
251 }
252
253 @Override
254 public String getToolTipText() {
255 return trn("{0} note", "{0} notes", noteData.getNotes().size(), noteData.getNotes().size());
256 }
257
258 @Override
259 public void mergeFrom(Layer from) {
260 throw new UnsupportedOperationException("Notes layer does not support merging yet");
261 }
262
263 @Override
264 public boolean isMergable(Layer other) {
265 return false;
266 }
267
268 @Override
269 public void visitBoundingBox(BoundingXYVisitor v) {
270 for (Note note : noteData.getNotes()) {
271 v.visit(note.getLatLon());
272 }
273 }
274
275 @Override
276 public Object getInfoComponent() {
277 StringBuilder sb = new StringBuilder();
278 sb.append(tr("Notes layer"))
279 .append('\n')
280 .append(tr("Total notes:"))
281 .append(' ')
282 .append(noteData.getNotes().size())
283 .append('\n')
284 .append(tr("Changes need uploading?"))
285 .append(' ')
286 .append(isModified());
287 return sb.toString();
288 }
289
290 @Override
291 public Action[] getMenuEntries() {
292 List<Action> actions = new ArrayList<>();
293 actions.add(LayerListDialog.getInstance().createShowHideLayerAction());
294 actions.add(LayerListDialog.getInstance().createDeleteLayerAction());
295 actions.add(new LayerListPopup.InfoAction(this));
296 actions.add(new LayerSaveAction(this));
297 actions.add(new LayerSaveAsAction(this));
298 return actions.toArray(new Action[actions.size()]);
299 }
300
301 @Override
302 public void mouseClicked(MouseEvent e) {
303 if (!SwingUtilities.isLeftMouseButton(e)) {
304 return;
305 }
306 Point clickPoint = e.getPoint();
307 double snapDistance = 10;
308 double minDistance = Double.MAX_VALUE;
309 final int iconHeight = ImageProvider.ImageSizes.SMALLICON.getAdjustedHeight();
310 Note closestNote = null;
311 for (Note note : noteData.getNotes()) {
312 Point notePoint = MainApplication.getMap().mapView.getPoint(note.getLatLon());
313 //move the note point to the center of the icon where users are most likely to click when selecting
314 notePoint.setLocation(notePoint.getX(), notePoint.getY() - iconHeight / 2);
315 double dist = clickPoint.distanceSq(notePoint);
316 if (minDistance > dist && clickPoint.distance(notePoint) < snapDistance) {
317 minDistance = dist;
318 closestNote = note;
319 }
320 }
321 noteData.setSelectedNote(closestNote);
322 }
323
324 @Override
325 public File createAndOpenSaveFileChooser() {
326 return SaveActionBase.createAndOpenSaveFileChooser(tr("Save Note file"), NoteExporter.FILE_FILTER);
327 }
328
329 @Override
330 public AbstractIOTask createUploadTask(ProgressMonitor monitor) {
331 return new UploadNoteLayerTask(this, monitor);
332 }
333
334 @Override
335 public void mousePressed(MouseEvent e) {
336 // Do nothing
337 }
338
339 @Override
340 public void mouseReleased(MouseEvent e) {
341 // Do nothing
342 }
343
344 @Override
345 public void mouseEntered(MouseEvent e) {
346 // Do nothing
347 }
348
349 @Override
350 public void mouseExited(MouseEvent e) {
351 // Do nothing
352 }
353
354 @Override
355 public void noteDataUpdated(NoteData data) {
356 invalidate();
357 }
358
359 @Override
360 public void selectedNoteChanged(NoteData noteData) {
361 invalidate();
362 }
363}
Note: See TracBrowser for help on using the repository browser.