001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.dialogs.changeset.query;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.BorderLayout;
007import java.awt.Color;
008import java.awt.GridBagConstraints;
009import java.awt.GridBagLayout;
010import java.awt.Insets;
011import java.awt.event.ItemEvent;
012import java.awt.event.ItemListener;
013import java.time.LocalDate;
014import java.time.LocalTime;
015import java.time.ZoneId;
016import java.time.ZonedDateTime;
017import java.time.format.DateTimeFormatter;
018import java.time.format.DateTimeParseException;
019import java.time.format.FormatStyle;
020import java.util.Date;
021
022import javax.swing.BorderFactory;
023import javax.swing.ButtonGroup;
024import javax.swing.JCheckBox;
025import javax.swing.JLabel;
026import javax.swing.JOptionPane;
027import javax.swing.JPanel;
028import javax.swing.JRadioButton;
029import javax.swing.JScrollPane;
030import javax.swing.text.JTextComponent;
031
032import org.openstreetmap.josm.Main;
033import org.openstreetmap.josm.gui.HelpAwareOptionPane;
034import org.openstreetmap.josm.gui.JosmUserIdentityManager;
035import org.openstreetmap.josm.gui.help.HelpUtil;
036import org.openstreetmap.josm.gui.preferences.server.UserNameValidator;
037import org.openstreetmap.josm.gui.util.GuiHelper;
038import org.openstreetmap.josm.gui.widgets.AbstractTextComponentValidator;
039import org.openstreetmap.josm.gui.widgets.BoundingBoxSelectionPanel;
040import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
041import org.openstreetmap.josm.gui.widgets.JosmTextField;
042import org.openstreetmap.josm.gui.widgets.SelectAllOnFocusGainedDecorator;
043import org.openstreetmap.josm.gui.widgets.VerticallyScrollablePanel;
044import org.openstreetmap.josm.io.ChangesetQuery;
045import org.openstreetmap.josm.tools.CheckParameterUtil;
046
047/**
048 * This panel allows to specify a changeset query
049 * @since 2689
050 */
051public class AdvancedChangesetQueryPanel extends JPanel {
052
053    private final JCheckBox cbUserRestriction = new JCheckBox();
054    private final JCheckBox cbOpenAndCloseRestrictions = new JCheckBox();
055    private final JCheckBox cbTimeRestrictions = new JCheckBox();
056    private final JCheckBox cbBoundingBoxRestriction = new JCheckBox();
057    private final UserRestrictionPanel pnlUserRestriction = new UserRestrictionPanel();
058    private final OpenAndCloseStateRestrictionPanel pnlOpenAndCloseRestriction = new OpenAndCloseStateRestrictionPanel();
059    private final TimeRestrictionPanel pnlTimeRestriction = new TimeRestrictionPanel();
060    private final BBoxRestrictionPanel pnlBoundingBoxRestriction = new BBoxRestrictionPanel();
061
062    /**
063     * Constructs a new {@code AdvancedChangesetQueryPanel}.
064     */
065    public AdvancedChangesetQueryPanel() {
066        build();
067    }
068
069    protected JPanel buildQueryPanel() {
070        ItemListener stateChangeHandler = new RestrictionGroupStateChangeHandler();
071        JPanel pnl = new VerticallyScrollablePanel(new GridBagLayout());
072        pnl.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
073        GridBagConstraints gc = new GridBagConstraints();
074
075        // -- select changesets by a specific user
076        //
077        gc.anchor = GridBagConstraints.NORTHWEST;
078        gc.weightx = 0.0;
079        gc.fill = GridBagConstraints.HORIZONTAL;
080        pnl.add(cbUserRestriction, gc);
081        cbUserRestriction.addItemListener(stateChangeHandler);
082
083        gc.gridx = 1;
084        gc.weightx = 1.0;
085        pnl.add(new JMultilineLabel(tr("Select changesets owned by specific users")), gc);
086
087        gc.gridy = 1;
088        gc.gridx = 1;
089        gc.weightx = 1.0;
090        pnl.add(pnlUserRestriction, gc);
091
092        // -- restricting the query to open and closed changesets
093        //
094        gc.gridy = 2;
095        gc.gridx = 0;
096        gc.anchor = GridBagConstraints.NORTHWEST;
097        gc.weightx = 0.0;
098        gc.fill = GridBagConstraints.HORIZONTAL;
099        pnl.add(cbOpenAndCloseRestrictions, gc);
100        cbOpenAndCloseRestrictions.addItemListener(stateChangeHandler);
101
102        gc.gridx = 1;
103        gc.weightx = 1.0;
104        pnl.add(new JMultilineLabel(tr("Select changesets depending on whether they are open or closed")), gc);
105
106        gc.gridy = 3;
107        gc.gridx = 1;
108        gc.weightx = 1.0;
109        pnl.add(pnlOpenAndCloseRestriction, gc);
110
111        // -- restricting the query to a specific time
112        //
113        gc.gridy = 4;
114        gc.gridx = 0;
115        gc.anchor = GridBagConstraints.NORTHWEST;
116        gc.weightx = 0.0;
117        gc.fill = GridBagConstraints.HORIZONTAL;
118        pnl.add(cbTimeRestrictions, gc);
119        cbTimeRestrictions.addItemListener(stateChangeHandler);
120
121        gc.gridx = 1;
122        gc.weightx = 1.0;
123        pnl.add(new JMultilineLabel(tr("Select changesets based on the date/time they have been created or closed")), gc);
124
125        gc.gridy = 5;
126        gc.gridx = 1;
127        gc.weightx = 1.0;
128        pnl.add(pnlTimeRestriction, gc);
129
130
131        // -- restricting the query to a specific bounding box
132        //
133        gc.gridy = 6;
134        gc.gridx = 0;
135        gc.anchor = GridBagConstraints.NORTHWEST;
136        gc.weightx = 0.0;
137        gc.fill = GridBagConstraints.HORIZONTAL;
138        pnl.add(cbBoundingBoxRestriction, gc);
139        cbBoundingBoxRestriction.addItemListener(stateChangeHandler);
140
141        gc.gridx = 1;
142        gc.weightx = 1.0;
143        pnl.add(new JMultilineLabel(tr("Select only changesets related to a specific bounding box")), gc);
144
145        gc.gridy = 7;
146        gc.gridx = 1;
147        gc.weightx = 1.0;
148        pnl.add(pnlBoundingBoxRestriction, gc);
149
150
151        gc.gridy = 8;
152        gc.gridx = 0;
153        gc.gridwidth = 2;
154        gc.fill = GridBagConstraints.BOTH;
155        gc.weightx = 1.0;
156        gc.weighty = 1.0;
157        pnl.add(new JPanel(), gc);
158
159        return pnl;
160    }
161
162    protected final void build() {
163        setLayout(new BorderLayout());
164        JScrollPane spQueryPanel = GuiHelper.embedInVerticalScrollPane(buildQueryPanel());
165        add(spQueryPanel, BorderLayout.CENTER);
166    }
167
168    public void startUserInput() {
169        restoreFromSettings();
170        pnlBoundingBoxRestriction.setVisible(cbBoundingBoxRestriction.isSelected());
171        pnlOpenAndCloseRestriction.setVisible(cbOpenAndCloseRestrictions.isSelected());
172        pnlTimeRestriction.setVisible(cbTimeRestrictions.isSelected());
173        pnlUserRestriction.setVisible(cbUserRestriction.isSelected());
174        pnlOpenAndCloseRestriction.startUserInput();
175        pnlUserRestriction.startUserInput();
176        pnlTimeRestriction.startUserInput();
177    }
178
179    public void displayMessageIfInvalid() {
180        if (cbUserRestriction.isSelected()) {
181            if (!pnlUserRestriction.isValidChangesetQuery()) {
182                pnlUserRestriction.displayMessageIfInvalid();
183            }
184        } else if (cbTimeRestrictions.isSelected()) {
185            if (!pnlTimeRestriction.isValidChangesetQuery()) {
186                pnlTimeRestriction.displayMessageIfInvalid();
187            }
188        } else if (cbBoundingBoxRestriction.isSelected()) {
189            if (!pnlBoundingBoxRestriction.isValidChangesetQuery()) {
190                pnlBoundingBoxRestriction.displayMessageIfInvalid();
191            }
192        }
193    }
194
195    /**
196     * Builds the changeset query based on the data entered in the form.
197     *
198     * @return the changeset query. null, if the data entered doesn't represent
199     * a valid changeset query.
200     */
201    public ChangesetQuery buildChangesetQuery() {
202        ChangesetQuery query = new ChangesetQuery();
203        if (cbUserRestriction.isSelected()) {
204            if (!pnlUserRestriction.isValidChangesetQuery())
205                return null;
206            pnlUserRestriction.fillInQuery(query);
207        }
208        if (cbOpenAndCloseRestrictions.isSelected()) {
209            // don't have to check whether it's valid. It always is.
210            pnlOpenAndCloseRestriction.fillInQuery(query);
211        }
212        if (cbBoundingBoxRestriction.isSelected()) {
213            if (!pnlBoundingBoxRestriction.isValidChangesetQuery())
214                return null;
215            pnlBoundingBoxRestriction.fillInQuery(query);
216        }
217        if (cbTimeRestrictions.isSelected()) {
218            if (!pnlTimeRestriction.isValidChangesetQuery())
219                return null;
220            pnlTimeRestriction.fillInQuery(query);
221        }
222        return query;
223    }
224
225    public void rememberSettings() {
226        Main.pref.put("changeset-query.advanced.user-restrictions", cbUserRestriction.isSelected());
227        Main.pref.put("changeset-query.advanced.open-restrictions", cbOpenAndCloseRestrictions.isSelected());
228        Main.pref.put("changeset-query.advanced.time-restrictions", cbTimeRestrictions.isSelected());
229        Main.pref.put("changeset-query.advanced.bbox-restrictions", cbBoundingBoxRestriction.isSelected());
230
231        pnlUserRestriction.rememberSettings();
232        pnlOpenAndCloseRestriction.rememberSettings();
233        pnlTimeRestriction.rememberSettings();
234    }
235
236    public void restoreFromSettings() {
237        cbUserRestriction.setSelected(Main.pref.getBoolean("changeset-query.advanced.user-restrictions", false));
238        cbOpenAndCloseRestrictions.setSelected(Main.pref.getBoolean("changeset-query.advanced.open-restrictions", false));
239        cbTimeRestrictions.setSelected(Main.pref.getBoolean("changeset-query.advanced.time-restrictions", false));
240        cbBoundingBoxRestriction.setSelected(Main.pref.getBoolean("changeset-query.advanced.bbox-restrictions", false));
241    }
242
243    class RestrictionGroupStateChangeHandler implements ItemListener {
244        protected void userRestrictionStateChanged() {
245            if (pnlUserRestriction == null)
246                return;
247            pnlUserRestriction.setVisible(cbUserRestriction.isSelected());
248        }
249
250        protected void openCloseRestrictionStateChanged() {
251            if (pnlOpenAndCloseRestriction == null)
252                return;
253            pnlOpenAndCloseRestriction.setVisible(cbOpenAndCloseRestrictions.isSelected());
254        }
255
256        protected void timeRestrictionsStateChanged() {
257            if (pnlTimeRestriction == null)
258                return;
259            pnlTimeRestriction.setVisible(cbTimeRestrictions.isSelected());
260        }
261
262        protected void boundingBoxRestrictionChanged() {
263            if (pnlBoundingBoxRestriction == null)
264                return;
265            pnlBoundingBoxRestriction.setVisible(cbBoundingBoxRestriction.isSelected());
266        }
267
268        @Override
269        public void itemStateChanged(ItemEvent e) {
270            if (e.getSource() == cbUserRestriction) {
271                userRestrictionStateChanged();
272            } else if (e.getSource() == cbOpenAndCloseRestrictions) {
273                openCloseRestrictionStateChanged();
274            } else if (e.getSource() == cbTimeRestrictions) {
275                timeRestrictionsStateChanged();
276            } else if (e.getSource() == cbBoundingBoxRestriction) {
277                boundingBoxRestrictionChanged();
278            }
279            validate();
280            repaint();
281        }
282    }
283
284    /**
285     * This is the panel for selecting whether the changeset query should be restricted to
286     * open or closed changesets
287     */
288    private static class OpenAndCloseStateRestrictionPanel extends JPanel {
289
290        private final JRadioButton rbOpenOnly = new JRadioButton();
291        private final JRadioButton rbClosedOnly = new JRadioButton();
292        private final JRadioButton rbBoth = new JRadioButton();
293
294        OpenAndCloseStateRestrictionPanel() {
295            build();
296        }
297
298        protected void build() {
299            setLayout(new GridBagLayout());
300            setBorder(BorderFactory.createCompoundBorder(
301                    BorderFactory.createEmptyBorder(3, 3, 3, 3),
302                    BorderFactory.createCompoundBorder(
303                            BorderFactory.createLineBorder(Color.GRAY),
304                            BorderFactory.createEmptyBorder(5, 5, 5, 5)
305                    )
306            ));
307            GridBagConstraints gc = new GridBagConstraints();
308            gc.anchor = GridBagConstraints.NORTHWEST;
309            gc.fill = GridBagConstraints.HORIZONTAL;
310            gc.weightx = 0.0;
311            add(rbOpenOnly, gc);
312
313            gc.gridx = 1;
314            gc.weightx = 1.0;
315            add(new JMultilineLabel(tr("Query open changesets only")), gc);
316
317            gc.gridy = 1;
318            gc.gridx = 0;
319            gc.weightx = 0.0;
320            add(rbClosedOnly, gc);
321
322            gc.gridx = 1;
323            gc.weightx = 1.0;
324            add(new JMultilineLabel(tr("Query closed changesets only")), gc);
325
326            gc.gridy = 2;
327            gc.gridx = 0;
328            gc.weightx = 0.0;
329            add(rbBoth, gc);
330
331            gc.gridx = 1;
332            gc.weightx = 1.0;
333            add(new JMultilineLabel(tr("Query both open and closed changesets")), gc);
334
335            ButtonGroup bgRestrictions = new ButtonGroup();
336            bgRestrictions.add(rbBoth);
337            bgRestrictions.add(rbClosedOnly);
338            bgRestrictions.add(rbOpenOnly);
339        }
340
341        public void startUserInput() {
342            restoreFromSettings();
343        }
344
345        public void fillInQuery(ChangesetQuery query) {
346            if (rbBoth.isSelected()) {
347                query.beingClosed(true);
348                query.beingOpen(true);
349            } else if (rbOpenOnly.isSelected()) {
350                query.beingOpen(true);
351            } else if (rbClosedOnly.isSelected()) {
352                query.beingClosed(true);
353            }
354        }
355
356        public void rememberSettings() {
357            String prefRoot = "changeset-query.advanced.open-restrictions";
358            if (rbBoth.isSelected()) {
359                Main.pref.put(prefRoot + ".query-type", "both");
360            } else if (rbOpenOnly.isSelected()) {
361                Main.pref.put(prefRoot + ".query-type", "open");
362            } else if (rbClosedOnly.isSelected()) {
363                Main.pref.put(prefRoot + ".query-type", "closed");
364            }
365        }
366
367        public void restoreFromSettings() {
368            String prefRoot = "changeset-query.advanced.open-restrictions";
369            String v = Main.pref.get(prefRoot + ".query-type", "open");
370            rbBoth.setSelected("both".equals(v));
371            rbOpenOnly.setSelected("open".equals(v));
372            rbClosedOnly.setSelected("closed".equals(v));
373        }
374    }
375
376    /**
377     * This is the panel for selecting whether the query should be restricted to a specific user
378     */
379    private static class UserRestrictionPanel extends JPanel {
380        private final ButtonGroup bgUserRestrictions = new ButtonGroup();
381        private final JRadioButton rbRestrictToMyself = new JRadioButton();
382        private final JRadioButton rbRestrictToUid = new JRadioButton();
383        private final JRadioButton rbRestrictToUserName = new JRadioButton();
384        private final JosmTextField tfUid = new JosmTextField(10);
385        private transient UidInputFieldValidator valUid;
386        private final JosmTextField tfUserName = new JosmTextField(10);
387        private transient UserNameValidator valUserName;
388        private final JMultilineLabel lblRestrictedToMyself = new JMultilineLabel(tr("Only changesets owned by myself"));
389
390        UserRestrictionPanel() {
391            build();
392        }
393
394        protected JPanel buildUidInputPanel() {
395            JPanel pnl = new JPanel(new GridBagLayout());
396            GridBagConstraints gc = new GridBagConstraints();
397            gc.fill = GridBagConstraints.HORIZONTAL;
398            gc.weightx = 0.0;
399            gc.insets = new Insets(0, 0, 0, 3);
400            pnl.add(new JLabel(tr("User ID:")), gc);
401
402            gc.gridx = 1;
403            pnl.add(tfUid, gc);
404            SelectAllOnFocusGainedDecorator.decorate(tfUid);
405            valUid = UidInputFieldValidator.decorate(tfUid);
406
407            // grab remaining space
408            gc.gridx = 2;
409            gc.weightx = 1.0;
410            pnl.add(new JPanel(), gc);
411            return pnl;
412        }
413
414        protected JPanel buildUserNameInputPanel() {
415            JPanel pnl = new JPanel(new GridBagLayout());
416            GridBagConstraints gc = new GridBagConstraints();
417            gc.fill = GridBagConstraints.HORIZONTAL;
418            gc.weightx = 0.0;
419            gc.insets = new Insets(0, 0, 0, 3);
420            pnl.add(new JLabel(tr("User name:")), gc);
421
422            gc.gridx = 1;
423            pnl.add(tfUserName, gc);
424            SelectAllOnFocusGainedDecorator.decorate(tfUserName);
425            valUserName = new UserNameValidator(tfUserName);
426
427            // grab remaining space
428            gc.gridx = 2;
429            gc.weightx = 1.0;
430            pnl.add(new JPanel(), gc);
431            return pnl;
432        }
433
434        protected void build() {
435            setLayout(new GridBagLayout());
436            setBorder(BorderFactory.createCompoundBorder(
437                    BorderFactory.createEmptyBorder(3, 3, 3, 3),
438                    BorderFactory.createCompoundBorder(
439                            BorderFactory.createLineBorder(Color.GRAY),
440                            BorderFactory.createEmptyBorder(5, 5, 5, 5)
441                    )
442            ));
443
444            ItemListener userRestrictionChangeHandler = new UserRestrictionChangedHandler();
445            GridBagConstraints gc = new GridBagConstraints();
446            gc.anchor = GridBagConstraints.NORTHWEST;
447            gc.gridx = 0;
448            gc.fill = GridBagConstraints.HORIZONTAL;
449            gc.weightx = 0.0;
450            add(rbRestrictToMyself, gc);
451            rbRestrictToMyself.addItemListener(userRestrictionChangeHandler);
452
453            gc.gridx = 1;
454            gc.fill = GridBagConstraints.HORIZONTAL;
455            gc.weightx = 1.0;
456            add(lblRestrictedToMyself, gc);
457
458            gc.gridx = 0;
459            gc.gridy = 1;
460            gc.fill = GridBagConstraints.HORIZONTAL;
461            gc.weightx = 0.0;
462            add(rbRestrictToUid, gc);
463            rbRestrictToUid.addItemListener(userRestrictionChangeHandler);
464
465            gc.gridx = 1;
466            gc.fill = GridBagConstraints.HORIZONTAL;
467            gc.weightx = 1.0;
468            add(new JMultilineLabel(tr("Only changesets owned by the user with the following user ID")), gc);
469
470            gc.gridx = 1;
471            gc.gridy = 2;
472            gc.fill = GridBagConstraints.HORIZONTAL;
473            gc.weightx = 1.0;
474            add(buildUidInputPanel(), gc);
475
476            gc.gridx = 0;
477            gc.gridy = 3;
478            gc.fill = GridBagConstraints.HORIZONTAL;
479            gc.weightx = 0.0;
480            add(rbRestrictToUserName, gc);
481            rbRestrictToUserName.addItemListener(userRestrictionChangeHandler);
482
483            gc.gridx = 1;
484            gc.fill = GridBagConstraints.HORIZONTAL;
485            gc.weightx = 1.0;
486            add(new JMultilineLabel(tr("Only changesets owned by the user with the following user name")), gc);
487
488            gc.gridx = 1;
489            gc.gridy = 4;
490            gc.fill = GridBagConstraints.HORIZONTAL;
491            gc.weightx = 1.0;
492            add(buildUserNameInputPanel(), gc);
493
494            bgUserRestrictions.add(rbRestrictToMyself);
495            bgUserRestrictions.add(rbRestrictToUid);
496            bgUserRestrictions.add(rbRestrictToUserName);
497        }
498
499        public void startUserInput() {
500            if (JosmUserIdentityManager.getInstance().isAnonymous()) {
501                lblRestrictedToMyself.setText(tr("Only changesets owned by myself (disabled. JOSM is currently run by an anonymous user)"));
502                rbRestrictToMyself.setEnabled(false);
503                if (rbRestrictToMyself.isSelected()) {
504                    rbRestrictToUid.setSelected(true);
505                }
506            } else {
507                lblRestrictedToMyself.setText(tr("Only changesets owned by myself"));
508                rbRestrictToMyself.setEnabled(true);
509                rbRestrictToMyself.setSelected(true);
510            }
511            restoreFromSettings();
512        }
513
514        /**
515         * Sets the query restrictions on <code>query</code> for changeset owner based
516         * restrictions.
517         *
518         * @param query the query. Must not be null.
519         * @throws IllegalArgumentException if query is null
520         * @throws IllegalStateException if one of the available values for query parameters in
521         * this panel isn't valid
522         */
523        public void fillInQuery(ChangesetQuery query) {
524            CheckParameterUtil.ensureParameterNotNull(query, "query");
525            if (rbRestrictToMyself.isSelected()) {
526                JosmUserIdentityManager im = JosmUserIdentityManager.getInstance();
527                if (im.isPartiallyIdentified()) {
528                    query.forUser(im.getUserName());
529                } else if (im.isFullyIdentified()) {
530                    query.forUser(im.getUserId());
531                } else
532                    throw new IllegalStateException(
533                            tr("Cannot restrict changeset query to the current user because the current user is anonymous"));
534            } else if (rbRestrictToUid.isSelected()) {
535                int uid = valUid.getUid();
536                if (uid > 0) {
537                    query.forUser(uid);
538                } else
539                    throw new IllegalStateException(tr("Current value ''{0}'' for user ID is not valid", tfUid.getText()));
540            } else if (rbRestrictToUserName.isSelected()) {
541                if (!valUserName.isValid())
542                    throw new IllegalStateException(
543                            tr("Cannot restrict the changeset query to the user name ''{0}''", tfUserName.getText()));
544                query.forUser(tfUserName.getText());
545            }
546        }
547
548        public boolean isValidChangesetQuery() {
549            if (rbRestrictToUid.isSelected())
550                return valUid.isValid();
551            else if (rbRestrictToUserName.isSelected())
552                return valUserName.isValid();
553            return true;
554        }
555
556        protected void alertInvalidUid() {
557            HelpAwareOptionPane.showOptionDialog(
558                    this,
559                    tr("Please enter a valid user ID"),
560                    tr("Invalid user ID"),
561                    JOptionPane.ERROR_MESSAGE,
562                    HelpUtil.ht("/Dialog/ChangesetQueryDialog#InvalidUserId")
563            );
564        }
565
566        protected void alertInvalidUserName() {
567            HelpAwareOptionPane.showOptionDialog(
568                    this,
569                    tr("Please enter a non-empty user name"),
570                    tr("Invalid user name"),
571                    JOptionPane.ERROR_MESSAGE,
572                    HelpUtil.ht("/Dialog/ChangesetQueryDialog#InvalidUserName")
573            );
574        }
575
576        public void displayMessageIfInvalid() {
577            if (rbRestrictToUid.isSelected()) {
578                if (!valUid.isValid()) {
579                    alertInvalidUid();
580                }
581            } else if (rbRestrictToUserName.isSelected()) {
582                if (!valUserName.isValid()) {
583                    alertInvalidUserName();
584                }
585            }
586        }
587
588        public void rememberSettings() {
589            String prefRoot = "changeset-query.advanced.user-restrictions";
590            if (rbRestrictToMyself.isSelected()) {
591                Main.pref.put(prefRoot + ".query-type", "mine");
592            } else if (rbRestrictToUid.isSelected()) {
593                Main.pref.put(prefRoot + ".query-type", "uid");
594            } else if (rbRestrictToUserName.isSelected()) {
595                Main.pref.put(prefRoot + ".query-type", "username");
596            }
597            Main.pref.put(prefRoot + ".uid", tfUid.getText());
598            Main.pref.put(prefRoot + ".username", tfUserName.getText());
599        }
600
601        public void restoreFromSettings() {
602            String prefRoot = "changeset-query.advanced.user-restrictions";
603            String v = Main.pref.get(prefRoot + ".query-type", "mine");
604            if ("mine".equals(v)) {
605                JosmUserIdentityManager im = JosmUserIdentityManager.getInstance();
606                if (im.isAnonymous()) {
607                    rbRestrictToUid.setSelected(true);
608                } else {
609                    rbRestrictToMyself.setSelected(true);
610                }
611            } else if ("uid".equals(v)) {
612                rbRestrictToUid.setSelected(true);
613            } else if ("username".equals(v)) {
614                rbRestrictToUserName.setSelected(true);
615            }
616            tfUid.setText(Main.pref.get(prefRoot + ".uid", ""));
617            if (!valUid.isValid()) {
618                tfUid.setText("");
619            }
620            tfUserName.setText(Main.pref.get(prefRoot + ".username", ""));
621        }
622
623        class UserRestrictionChangedHandler implements ItemListener {
624            @Override
625            public void itemStateChanged(ItemEvent e) {
626                tfUid.setEnabled(rbRestrictToUid.isSelected());
627                tfUserName.setEnabled(rbRestrictToUserName.isSelected());
628                if (rbRestrictToUid.isSelected()) {
629                    tfUid.requestFocusInWindow();
630                } else if (rbRestrictToUserName.isSelected()) {
631                    tfUserName.requestFocusInWindow();
632                }
633            }
634        }
635    }
636
637    /**
638     * This is the panel to apply a time restriction to the changeset query
639     */
640    private static class TimeRestrictionPanel extends JPanel {
641
642        private final JRadioButton rbClosedAfter = new JRadioButton();
643        private final JRadioButton rbClosedAfterAndCreatedBefore = new JRadioButton();
644        private final JosmTextField tfClosedAfterDate1 = new JosmTextField();
645        private transient DateValidator valClosedAfterDate1;
646        private final JosmTextField tfClosedAfterTime1 = new JosmTextField();
647        private transient TimeValidator valClosedAfterTime1;
648        private final JosmTextField tfClosedAfterDate2 = new JosmTextField();
649        private transient DateValidator valClosedAfterDate2;
650        private final JosmTextField tfClosedAfterTime2 = new JosmTextField();
651        private transient TimeValidator valClosedAfterTime2;
652        private final JosmTextField tfCreatedBeforeDate = new JosmTextField();
653        private transient DateValidator valCreatedBeforeDate;
654        private final JosmTextField tfCreatedBeforeTime = new JosmTextField();
655        private transient TimeValidator valCreatedBeforeTime;
656
657        TimeRestrictionPanel() {
658            build();
659        }
660
661        protected JPanel buildClosedAfterInputPanel() {
662            JPanel pnl = new JPanel(new GridBagLayout());
663            GridBagConstraints gc = new GridBagConstraints();
664            gc.fill = GridBagConstraints.HORIZONTAL;
665            gc.weightx = 0.0;
666            gc.insets = new Insets(0, 0, 0, 3);
667            pnl.add(new JLabel(tr("Date: ")), gc);
668
669            gc.gridx = 1;
670            gc.weightx = 0.7;
671            pnl.add(tfClosedAfterDate1, gc);
672            SelectAllOnFocusGainedDecorator.decorate(tfClosedAfterDate1);
673            valClosedAfterDate1 = DateValidator.decorate(tfClosedAfterDate1);
674            tfClosedAfterDate1.setToolTipText(valClosedAfterDate1.getStandardTooltipTextAsHtml());
675
676            gc.gridx = 2;
677            gc.weightx = 0.0;
678            pnl.add(new JLabel(tr("Time:")), gc);
679
680            gc.gridx = 3;
681            gc.weightx = 0.3;
682            pnl.add(tfClosedAfterTime1, gc);
683            SelectAllOnFocusGainedDecorator.decorate(tfClosedAfterTime1);
684            valClosedAfterTime1 = TimeValidator.decorate(tfClosedAfterTime1);
685            tfClosedAfterTime1.setToolTipText(valClosedAfterTime1.getStandardTooltipTextAsHtml());
686            return pnl;
687        }
688
689        protected JPanel buildClosedAfterAndCreatedBeforeInputPanel() {
690            JPanel pnl = new JPanel(new GridBagLayout());
691            GridBagConstraints gc = new GridBagConstraints();
692            gc.fill = GridBagConstraints.HORIZONTAL;
693            gc.weightx = 0.0;
694            gc.insets = new Insets(0, 0, 0, 3);
695            pnl.add(new JLabel(tr("Closed after - ")), gc);
696
697            gc.gridx = 1;
698            gc.fill = GridBagConstraints.HORIZONTAL;
699            gc.weightx = 0.0;
700            gc.insets = new Insets(0, 0, 0, 3);
701            pnl.add(new JLabel(tr("Date:")), gc);
702
703            gc.gridx = 2;
704            gc.weightx = 0.7;
705            pnl.add(tfClosedAfterDate2, gc);
706            SelectAllOnFocusGainedDecorator.decorate(tfClosedAfterDate2);
707            valClosedAfterDate2 = DateValidator.decorate(tfClosedAfterDate2);
708            tfClosedAfterDate2.setToolTipText(valClosedAfterDate2.getStandardTooltipTextAsHtml());
709            gc.gridx = 3;
710            gc.weightx = 0.0;
711            pnl.add(new JLabel(tr("Time:")), gc);
712
713            gc.gridx = 4;
714            gc.weightx = 0.3;
715            pnl.add(tfClosedAfterTime2, gc);
716            SelectAllOnFocusGainedDecorator.decorate(tfClosedAfterTime2);
717            valClosedAfterTime2 = TimeValidator.decorate(tfClosedAfterTime2);
718            tfClosedAfterTime2.setToolTipText(valClosedAfterTime2.getStandardTooltipTextAsHtml());
719
720            gc.gridy = 1;
721            gc.gridx = 0;
722            gc.fill = GridBagConstraints.HORIZONTAL;
723            gc.weightx = 0.0;
724            gc.insets = new Insets(0, 0, 0, 3);
725            pnl.add(new JLabel(tr("Created before - ")), gc);
726
727            gc.gridx = 1;
728            gc.fill = GridBagConstraints.HORIZONTAL;
729            gc.weightx = 0.0;
730            gc.insets = new Insets(0, 0, 0, 3);
731            pnl.add(new JLabel(tr("Date:")), gc);
732
733            gc.gridx = 2;
734            gc.weightx = 0.7;
735            pnl.add(tfCreatedBeforeDate, gc);
736            SelectAllOnFocusGainedDecorator.decorate(tfCreatedBeforeDate);
737            valCreatedBeforeDate = DateValidator.decorate(tfCreatedBeforeDate);
738            tfCreatedBeforeDate.setToolTipText(valCreatedBeforeDate.getStandardTooltipTextAsHtml());
739
740            gc.gridx = 3;
741            gc.weightx = 0.0;
742            pnl.add(new JLabel(tr("Time:")), gc);
743
744            gc.gridx = 4;
745            gc.weightx = 0.3;
746            pnl.add(tfCreatedBeforeTime, gc);
747            SelectAllOnFocusGainedDecorator.decorate(tfCreatedBeforeTime);
748            valCreatedBeforeTime = TimeValidator.decorate(tfCreatedBeforeTime);
749            tfCreatedBeforeTime.setToolTipText(valCreatedBeforeDate.getStandardTooltipTextAsHtml());
750
751            return pnl;
752        }
753
754        protected void build() {
755            setLayout(new GridBagLayout());
756            setBorder(BorderFactory.createCompoundBorder(
757                    BorderFactory.createEmptyBorder(3, 3, 3, 3),
758                    BorderFactory.createCompoundBorder(
759                            BorderFactory.createLineBorder(Color.GRAY),
760                            BorderFactory.createEmptyBorder(5, 5, 5, 5)
761                    )
762            ));
763
764            // -- changesets closed after a specific date/time
765            //
766            GridBagConstraints gc = new GridBagConstraints();
767            gc.anchor = GridBagConstraints.NORTHWEST;
768            gc.gridx = 0;
769            gc.fill = GridBagConstraints.HORIZONTAL;
770            gc.weightx = 0.0;
771            add(rbClosedAfter, gc);
772
773            gc.gridx = 1;
774            gc.fill = GridBagConstraints.HORIZONTAL;
775            gc.weightx = 1.0;
776            add(new JMultilineLabel(tr("Only changesets closed after the following date/time")), gc);
777
778            gc.gridx = 1;
779            gc.gridy = 1;
780            gc.fill = GridBagConstraints.HORIZONTAL;
781            gc.weightx = 1.0;
782            add(buildClosedAfterInputPanel(), gc);
783
784            // -- changesets closed after a specific date/time and created before a specific date time
785            //
786            gc = new GridBagConstraints();
787            gc.anchor = GridBagConstraints.NORTHWEST;
788            gc.gridy = 2;
789            gc.gridx = 0;
790            gc.fill = GridBagConstraints.HORIZONTAL;
791            gc.weightx = 0.0;
792            add(rbClosedAfterAndCreatedBefore, gc);
793
794            gc.gridx = 1;
795            gc.fill = GridBagConstraints.HORIZONTAL;
796            gc.weightx = 1.0;
797            add(new JMultilineLabel(tr("Only changesets closed after and created before a specific date/time")), gc);
798
799            gc.gridx = 1;
800            gc.gridy = 3;
801            gc.fill = GridBagConstraints.HORIZONTAL;
802            gc.weightx = 1.0;
803            add(buildClosedAfterAndCreatedBeforeInputPanel(), gc);
804
805            ButtonGroup bg = new ButtonGroup();
806            bg.add(rbClosedAfter);
807            bg.add(rbClosedAfterAndCreatedBefore);
808
809            ItemListener restrictionChangeHandler = new TimeRestrictionChangedHandler();
810            rbClosedAfter.addItemListener(restrictionChangeHandler);
811            rbClosedAfterAndCreatedBefore.addItemListener(restrictionChangeHandler);
812
813            rbClosedAfter.setSelected(true);
814        }
815
816        public boolean isValidChangesetQuery() {
817            if (rbClosedAfter.isSelected())
818                return valClosedAfterDate1.isValid() && valClosedAfterTime1.isValid();
819            else if (rbClosedAfterAndCreatedBefore.isSelected())
820                return valClosedAfterDate2.isValid() && valClosedAfterTime2.isValid()
821                && valCreatedBeforeDate.isValid() && valCreatedBeforeTime.isValid();
822            // should not happen
823            return true;
824        }
825
826        class TimeRestrictionChangedHandler implements ItemListener {
827            @Override
828            public void itemStateChanged(ItemEvent e) {
829                tfClosedAfterDate1.setEnabled(rbClosedAfter.isSelected());
830                tfClosedAfterTime1.setEnabled(rbClosedAfter.isSelected());
831
832                tfClosedAfterDate2.setEnabled(rbClosedAfterAndCreatedBefore.isSelected());
833                tfClosedAfterTime2.setEnabled(rbClosedAfterAndCreatedBefore.isSelected());
834                tfCreatedBeforeDate.setEnabled(rbClosedAfterAndCreatedBefore.isSelected());
835                tfCreatedBeforeTime.setEnabled(rbClosedAfterAndCreatedBefore.isSelected());
836            }
837        }
838
839        public void startUserInput() {
840            restoreFromSettings();
841        }
842
843        public void fillInQuery(ChangesetQuery query) {
844            if (!isValidChangesetQuery())
845                throw new IllegalStateException(tr("Cannot build changeset query with time based restrictions. Input is not valid."));
846            if (rbClosedAfter.isSelected()) {
847                LocalDate d1 = valClosedAfterDate1.getDate();
848                LocalTime d2 = valClosedAfterTime1.getDate();
849                final Date d3 = new Date(d1.atTime(d2).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
850                query.closedAfter(d3);
851            } else if (rbClosedAfterAndCreatedBefore.isSelected()) {
852                LocalDate d1 = valClosedAfterDate2.getDate();
853                LocalTime d2 = valClosedAfterTime2.getDate();
854                Date d3 = new Date(d1.atTime(d2).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
855
856                d1 = valCreatedBeforeDate.getDate();
857                d2 = valCreatedBeforeTime.getDate();
858                Date d4 = new Date(d1.atTime(d2).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
859
860                query.closedAfterAndCreatedBefore(d3, d4);
861            }
862        }
863
864        public void displayMessageIfInvalid() {
865            if (isValidChangesetQuery())
866                return;
867            HelpAwareOptionPane.showOptionDialog(
868                    this,
869                    tr(
870                            "<html>Please enter valid date/time values to restrict<br>"
871                            + "the query to a specific time range.</html>"
872                    ),
873                    tr("Invalid date/time values"),
874                    JOptionPane.ERROR_MESSAGE,
875                    HelpUtil.ht("/Dialog/ChangesetQueryDialog#InvalidDateTimeValues")
876            );
877        }
878
879        public void rememberSettings() {
880            String prefRoot = "changeset-query.advanced.time-restrictions";
881            if (rbClosedAfter.isSelected()) {
882                Main.pref.put(prefRoot + ".query-type", "closed-after");
883            } else if (rbClosedAfterAndCreatedBefore.isSelected()) {
884                Main.pref.put(prefRoot + ".query-type", "closed-after-created-before");
885            }
886            Main.pref.put(prefRoot + ".closed-after.date", tfClosedAfterDate1.getText());
887            Main.pref.put(prefRoot + ".closed-after.time", tfClosedAfterTime1.getText());
888            Main.pref.put(prefRoot + ".closed-created.closed.date", tfClosedAfterDate2.getText());
889            Main.pref.put(prefRoot + ".closed-created.closed.time", tfClosedAfterTime2.getText());
890            Main.pref.put(prefRoot + ".closed-created.created.date", tfCreatedBeforeDate.getText());
891            Main.pref.put(prefRoot + ".closed-created.created.time", tfCreatedBeforeTime.getText());
892        }
893
894        public void restoreFromSettings() {
895            String prefRoot = "changeset-query.advanced.open-restrictions";
896            String v = Main.pref.get(prefRoot + ".query-type", "closed-after");
897            rbClosedAfter.setSelected("closed-after".equals(v));
898            rbClosedAfterAndCreatedBefore.setSelected("closed-after-created-before".equals(v));
899            if (!rbClosedAfter.isSelected() && !rbClosedAfterAndCreatedBefore.isSelected()) {
900                rbClosedAfter.setSelected(true);
901            }
902            tfClosedAfterDate1.setText(Main.pref.get(prefRoot + ".closed-after.date", ""));
903            tfClosedAfterTime1.setText(Main.pref.get(prefRoot + ".closed-after.time", ""));
904            tfClosedAfterDate2.setText(Main.pref.get(prefRoot + ".closed-created.closed.date", ""));
905            tfClosedAfterTime2.setText(Main.pref.get(prefRoot + ".closed-created.closed.time", ""));
906            tfCreatedBeforeDate.setText(Main.pref.get(prefRoot + ".closed-created.created.date", ""));
907            tfCreatedBeforeTime.setText(Main.pref.get(prefRoot + ".closed-created.created.time", ""));
908            if (!valClosedAfterDate1.isValid()) {
909                tfClosedAfterDate1.setText("");
910            }
911            if (!valClosedAfterTime1.isValid()) {
912                tfClosedAfterTime1.setText("");
913            }
914            if (!valClosedAfterDate2.isValid()) {
915                tfClosedAfterDate2.setText("");
916            }
917            if (!valClosedAfterTime2.isValid()) {
918                tfClosedAfterTime2.setText("");
919            }
920            if (!valCreatedBeforeDate.isValid()) {
921                tfCreatedBeforeDate.setText("");
922            }
923            if (!valCreatedBeforeTime.isValid()) {
924                tfCreatedBeforeTime.setText("");
925            }
926        }
927    }
928
929    private static class BBoxRestrictionPanel extends BoundingBoxSelectionPanel {
930        BBoxRestrictionPanel() {
931            setBorder(BorderFactory.createCompoundBorder(
932                    BorderFactory.createEmptyBorder(3, 3, 3, 3),
933                    BorderFactory.createCompoundBorder(
934                            BorderFactory.createLineBorder(Color.GRAY),
935                            BorderFactory.createEmptyBorder(5, 5, 5, 5)
936                    )
937            ));
938        }
939
940        public boolean isValidChangesetQuery() {
941            return getBoundingBox() != null;
942        }
943
944        public void fillInQuery(ChangesetQuery query) {
945            if (!isValidChangesetQuery())
946                throw new IllegalStateException(tr("Cannot restrict the changeset query to a specific bounding box. The input is invalid."));
947            query.inBbox(getBoundingBox());
948        }
949
950        public void displayMessageIfInvalid() {
951            if (isValidChangesetQuery())
952                return;
953            HelpAwareOptionPane.showOptionDialog(
954                    this,
955                    tr(
956                            "<html>Please enter valid longitude/latitude values to restrict<br>" +
957                            "the changeset query to a specific bounding box.</html>"
958                    ),
959                    tr("Invalid bounding box"),
960                    JOptionPane.ERROR_MESSAGE,
961                    HelpUtil.ht("/Dialog/ChangesetQueryDialog#InvalidBoundingBox")
962            );
963        }
964    }
965
966    /**
967     * Validator for user ids entered in a {@link JTextComponent}.
968     *
969     */
970    private static class UidInputFieldValidator extends AbstractTextComponentValidator {
971        UidInputFieldValidator(JTextComponent tc) {
972            super(tc);
973        }
974
975        public static UidInputFieldValidator decorate(JTextComponent tc) {
976            return new UidInputFieldValidator(tc);
977        }
978
979        @Override
980        public boolean isValid() {
981            return getUid() > 0;
982        }
983
984        @Override
985        public void validate() {
986            String value = getComponent().getText();
987            if (value == null || value.trim().isEmpty()) {
988                feedbackInvalid("");
989                return;
990            }
991            try {
992                int uid = Integer.parseInt(value);
993                if (uid <= 0) {
994                    feedbackInvalid(tr("The current value is not a valid user ID. Please enter an integer value > 0"));
995                    return;
996                }
997            } catch (NumberFormatException e) {
998                feedbackInvalid(tr("The current value is not a valid user ID. Please enter an integer value > 0"));
999                return;
1000            }
1001            feedbackValid(tr("Please enter an integer value > 0"));
1002        }
1003
1004        public int getUid() {
1005            String value = getComponent().getText();
1006            if (value == null || value.trim().isEmpty()) return 0;
1007            try {
1008                int uid = Integer.parseInt(value.trim());
1009                if (uid > 0)
1010                    return uid;
1011                return 0;
1012            } catch (NumberFormatException e) {
1013                return 0;
1014            }
1015        }
1016    }
1017
1018    /**
1019     * Validates dates entered as text in a {@link JTextComponent}. Validates the input
1020     * on the fly and gives feedback about whether the date is valid or not.
1021     *
1022     * Dates can be entered in one of four standard formats defined for the current locale.
1023     */
1024    private static class DateValidator extends AbstractTextComponentValidator {
1025        DateValidator(JTextComponent tc) {
1026            super(tc);
1027        }
1028
1029        public static DateValidator decorate(JTextComponent tc) {
1030            return new DateValidator(tc);
1031        }
1032
1033        @Override
1034        public boolean isValid() {
1035            return getDate() != null;
1036        }
1037
1038        public String getStandardTooltipTextAsHtml() {
1039            return "<html>" + getStandardTooltipText() + "</html>";
1040        }
1041
1042        public String getStandardTooltipText() {
1043            final ZonedDateTime now = ZonedDateTime.now();
1044            return tr(
1045                    "Please enter a date in the usual format for your locale.<br>"
1046                    + "Example: {0}<br>"
1047                    + "Example: {1}<br>"
1048                    + "Example: {2}<br>"
1049                    + "Example: {3}<br>",
1050                    DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).format(now),
1051                    DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).format(now),
1052                    DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG).format(now),
1053                    DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL).format(now)
1054            );
1055        }
1056
1057        @Override
1058        public void validate() {
1059            if (!isValid()) {
1060                String msg = "<html>The current value isn't a valid date.<br>" + getStandardTooltipText()+ "</html>";
1061                feedbackInvalid(msg);
1062                return;
1063            } else {
1064                String msg = "<html>" + getStandardTooltipText() + "</html>";
1065                feedbackValid(msg);
1066            }
1067        }
1068
1069        public LocalDate getDate() {
1070            for (final FormatStyle format: FormatStyle.values()) {
1071                DateTimeFormatter df = DateTimeFormatter.ofLocalizedDate(format);
1072                try {
1073                    return LocalDate.parse(getComponent().getText(), df);
1074                } catch (DateTimeParseException e) {
1075                    // Try next format
1076                    Main.trace(e);
1077                }
1078            }
1079            return null;
1080        }
1081    }
1082
1083    /**
1084     * Validates time values entered as text in a {@link JTextComponent}. Validates the input
1085     * on the fly and gives feedback about whether the time value is valid or not.
1086     *
1087     * Time values can be entered in one of four standard formats defined for the current locale.
1088     */
1089    private static class TimeValidator extends AbstractTextComponentValidator {
1090        TimeValidator(JTextComponent tc) {
1091            super(tc);
1092        }
1093
1094        public static TimeValidator decorate(JTextComponent tc) {
1095            return new TimeValidator(tc);
1096        }
1097
1098        @Override
1099        public boolean isValid() {
1100            if (getComponent().getText().trim().isEmpty())
1101                return true;
1102            return getDate() != null;
1103        }
1104
1105        public String getStandardTooltipTextAsHtml() {
1106            return "<html>" + getStandardTooltipText() + "</html>";
1107        }
1108
1109        public String getStandardTooltipText() {
1110            final ZonedDateTime now = ZonedDateTime.now();
1111            return tr(
1112                    "Please enter a valid time in the usual format for your locale.<br>"
1113                    + "Example: {0}<br>"
1114                    + "Example: {1}<br>"
1115                    + "Example: {2}<br>"
1116                    + "Example: {3}<br>",
1117                    DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).format(now),
1118                    DateTimeFormatter.ofLocalizedTime(FormatStyle.MEDIUM).format(now),
1119                    DateTimeFormatter.ofLocalizedTime(FormatStyle.LONG).format(now),
1120                    DateTimeFormatter.ofLocalizedTime(FormatStyle.FULL).format(now)
1121            );
1122        }
1123
1124        @Override
1125        public void validate() {
1126            if (!isValid()) {
1127                String msg = "<html>The current value isn't a valid time.<br>" + getStandardTooltipText() + "</html>";
1128                feedbackInvalid(msg);
1129                return;
1130            } else {
1131                String msg = "<html>" + getStandardTooltipText() + "</html>";
1132                feedbackValid(msg);
1133            }
1134        }
1135
1136        public LocalTime getDate() {
1137            if (getComponent().getText().trim().isEmpty())
1138                return LocalTime.MIDNIGHT;
1139
1140            for (final FormatStyle format: FormatStyle.values()) {
1141                DateTimeFormatter df = DateTimeFormatter.ofLocalizedTime(format);
1142                try {
1143                    return LocalTime.parse(getComponent().getText(), df);
1144                } catch (DateTimeParseException e) {
1145                    // Try next format
1146                    Main.trace(e);
1147                }
1148            }
1149            return LocalTime.MIDNIGHT;
1150        }
1151    }
1152}