001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.preferences.projection;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Dimension;
007import java.awt.GridBagLayout;
008import java.awt.event.ActionListener;
009import java.io.Serializable;
010import java.util.ArrayList;
011import java.util.Collection;
012import java.util.Collections;
013import java.util.Comparator;
014import java.util.List;
015import java.util.Locale;
016import java.util.regex.Matcher;
017import java.util.regex.Pattern;
018
019import javax.swing.AbstractListModel;
020import javax.swing.JList;
021import javax.swing.JPanel;
022import javax.swing.JScrollPane;
023import javax.swing.event.DocumentEvent;
024import javax.swing.event.DocumentListener;
025import javax.swing.event.ListSelectionEvent;
026import javax.swing.event.ListSelectionListener;
027
028import org.openstreetmap.josm.data.projection.Projection;
029import org.openstreetmap.josm.data.projection.Projections;
030import org.openstreetmap.josm.gui.widgets.JosmTextField;
031import org.openstreetmap.josm.tools.GBC;
032
033/**
034 * Projection choice that lists all known projects by code.
035 */
036public class CodeProjectionChoice extends AbstractProjectionChoice implements SubPrefsOptions {
037
038    private String code;
039
040    /**
041     * Constructs a new {@code CodeProjectionChoice}.
042     */
043    public CodeProjectionChoice() {
044        super(tr("By Code (EPSG)"), /* NO-ICON */ "core:code");
045    }
046
047    private static class CodeSelectionPanel extends JPanel implements ListSelectionListener, DocumentListener {
048
049        public JosmTextField filter;
050        private ProjectionCodeListModel model;
051        public JList<String> selectionList;
052        private final List<String> data;
053        private final List<String> filteredData;
054        private static final String DEFAULT_CODE = "EPSG:3857";
055        private String lastCode = DEFAULT_CODE;
056        private final transient ActionListener listener;
057
058        CodeSelectionPanel(String initialCode, ActionListener listener) {
059            this.listener = listener;
060            data = new ArrayList<>(Projections.getAllProjectionCodes());
061            Collections.sort(data, new CodeComparator());
062            filteredData = new ArrayList<>(data);
063            build();
064            setCode(initialCode != null ? initialCode : DEFAULT_CODE);
065            selectionList.addListSelectionListener(this);
066        }
067
068        /**
069         * Comparator that compares the number part of the code numerically.
070         */
071        private static class CodeComparator implements Comparator<String>, Serializable {
072            private static final long serialVersionUID = 1L;
073            private final Pattern codePattern = Pattern.compile("([a-zA-Z]+):(\\d+)");
074
075            @Override
076            public int compare(String c1, String c2) {
077                Matcher matcher1 = codePattern.matcher(c1);
078                Matcher matcher2 = codePattern.matcher(c2);
079                if (matcher1.matches()) {
080                    if (matcher2.matches()) {
081                        int cmp1 = matcher1.group(1).compareTo(matcher2.group(1));
082                        if (cmp1 != 0) return cmp1;
083                        int num1 = Integer.parseInt(matcher1.group(2));
084                        int num2 = Integer.parseInt(matcher2.group(2));
085                        return Integer.compare(num1, num2);
086                    } else
087                        return -1;
088                } else if (matcher2.matches())
089                    return 1;
090                return c1.compareTo(c2);
091            }
092        }
093
094        /**
095         * List model for the filtered view on the list of all codes.
096         */
097        private class ProjectionCodeListModel extends AbstractListModel<String> {
098            @Override
099            public int getSize() {
100                return filteredData.size();
101            }
102
103            @Override
104            public String getElementAt(int index) {
105                if (index >= 0 && index < filteredData.size())
106                    return filteredData.get(index);
107                else
108                    return null;
109            }
110
111            public void fireContentsChanged() {
112                fireContentsChanged(this, 0, this.getSize()-1);
113            }
114        }
115
116        private void build() {
117            filter = new JosmTextField(30);
118            filter.setColumns(10);
119            filter.getDocument().addDocumentListener(this);
120
121            selectionList = new JList<>(data.toArray(new String[0]));
122            selectionList.setModel(model = new ProjectionCodeListModel());
123            JScrollPane scroll = new JScrollPane(selectionList);
124            scroll.setPreferredSize(new Dimension(200, 214));
125
126            this.setLayout(new GridBagLayout());
127            this.add(filter, GBC.eol().weight(1.0, 0.0));
128            this.add(scroll, GBC.eol());
129        }
130
131        public String getCode() {
132            int idx = selectionList.getSelectedIndex();
133            if (idx == -1) return lastCode;
134            return filteredData.get(selectionList.getSelectedIndex());
135        }
136
137        public final void setCode(String code) {
138            int idx = filteredData.indexOf(code);
139            if (idx != -1) {
140                selectionList.setSelectedIndex(idx);
141                selectionList.ensureIndexIsVisible(idx);
142            }
143        }
144
145        @Override
146        public void valueChanged(ListSelectionEvent e) {
147            listener.actionPerformed(null);
148            lastCode = getCode();
149        }
150
151        @Override
152        public void insertUpdate(DocumentEvent e) {
153            updateFilter();
154        }
155
156        @Override
157        public void removeUpdate(DocumentEvent e) {
158            updateFilter();
159        }
160
161        @Override
162        public void changedUpdate(DocumentEvent e) {
163            updateFilter();
164        }
165
166        private void updateFilter() {
167            filteredData.clear();
168            String filterTxt = filter.getText().trim().toLowerCase(Locale.ENGLISH);
169            for (String code : data) {
170                if (code.toLowerCase(Locale.ENGLISH).contains(filterTxt)) {
171                    filteredData.add(code);
172                }
173            }
174            model.fireContentsChanged();
175            int idx =  filteredData.indexOf(lastCode);
176            if (idx == -1) {
177                selectionList.clearSelection();
178                if (selectionList.getModel().getSize() > 0) {
179                    selectionList.ensureIndexIsVisible(0);
180                }
181            } else {
182                selectionList.setSelectedIndex(idx);
183                selectionList.ensureIndexIsVisible(idx);
184            }
185        }
186    }
187
188    @Override
189    public Projection getProjection() {
190        return Projections.getProjectionByCode(code);
191    }
192
193    @Override
194    public String getCurrentCode() {
195        // not needed - getProjection() is overridden
196        throw new UnsupportedOperationException();
197    }
198
199    @Override
200    public String getProjectionName() {
201        // not needed - getProjection() is overridden
202        throw new UnsupportedOperationException();
203    }
204
205    @Override
206    public void setPreferences(Collection<String> args) {
207        if (args != null && !args.isEmpty()) {
208            code = args.iterator().next();
209        }
210    }
211
212    @Override
213    public JPanel getPreferencePanel(ActionListener listener) {
214        return new CodeSelectionPanel(code, listener);
215    }
216
217    @Override
218    public Collection<String> getPreferences(JPanel panel) {
219        if (!(panel instanceof CodeSelectionPanel)) {
220            throw new IllegalArgumentException("Unsupported panel: "+panel);
221        }
222        CodeSelectionPanel csPanel = (CodeSelectionPanel) panel;
223        return Collections.singleton(csPanel.getCode());
224    }
225
226    /* don't return all possible codes - this projection choice it too generic */
227    @Override
228    public String[] allCodes() {
229        return new String[0];
230    }
231
232    /* not needed since allCodes() returns empty array */
233    @Override
234    public Collection<String> getPreferencesFromCode(String code) {
235        return null;
236    }
237
238    @Override
239    public boolean showProjectionCode() {
240        return true;
241    }
242
243    @Override
244    public boolean showProjectionName() {
245        return true;
246    }
247
248}