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