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 = "&lt;Anonymous&gt;";
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("&#xA;", "<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}