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