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

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

see #11217 - forward note tooltips mouse wheel scroll events to map frame

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