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