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 final JosmTextField filter = new JosmTextField(30);
050        private final ProjectionCodeListModel model = new ProjectionCodeListModel();
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         * List model for the filtered view on the list of all codes.
070         */
071        private class ProjectionCodeListModel extends AbstractListModel<String> {
072            @Override
073            public int getSize() {
074                return filteredData.size();
075            }
076
077            @Override
078            public String getElementAt(int index) {
079                if (index >= 0 && index < filteredData.size())
080                    return filteredData.get(index);
081                else
082                    return null;
083            }
084
085            public void fireContentsChanged() {
086                fireContentsChanged(this, 0, this.getSize()-1);
087            }
088        }
089
090        private void build() {
091            filter.setColumns(10);
092            filter.getDocument().addDocumentListener(this);
093
094            selectionList = new JList<>(data.toArray(new String[0]));
095            selectionList.setModel(model);
096            JScrollPane scroll = new JScrollPane(selectionList);
097            scroll.setPreferredSize(new Dimension(200, 214));
098
099            this.setLayout(new GridBagLayout());
100            this.add(filter, GBC.eol().weight(1.0, 0.0));
101            this.add(scroll, GBC.eol());
102        }
103
104        public String getCode() {
105            int idx = selectionList.getSelectedIndex();
106            if (idx == -1)
107                return lastCode;
108            return filteredData.get(selectionList.getSelectedIndex());
109        }
110
111        public final void setCode(String code) {
112            int idx = filteredData.indexOf(code);
113            if (idx != -1) {
114                selectionList.setSelectedIndex(idx);
115                selectionList.ensureIndexIsVisible(idx);
116            }
117        }
118
119        @Override
120        public void valueChanged(ListSelectionEvent e) {
121            listener.actionPerformed(null);
122            lastCode = getCode();
123        }
124
125        @Override
126        public void insertUpdate(DocumentEvent e) {
127            updateFilter();
128        }
129
130        @Override
131        public void removeUpdate(DocumentEvent e) {
132            updateFilter();
133        }
134
135        @Override
136        public void changedUpdate(DocumentEvent e) {
137            updateFilter();
138        }
139
140        private void updateFilter() {
141            filteredData.clear();
142            String filterTxt = filter.getText().trim().toLowerCase(Locale.ENGLISH);
143            for (String code : data) {
144                if (code.toLowerCase(Locale.ENGLISH).contains(filterTxt)) {
145                    filteredData.add(code);
146                }
147            }
148            model.fireContentsChanged();
149            int idx = filteredData.indexOf(lastCode);
150            if (idx == -1) {
151                selectionList.clearSelection();
152                if (selectionList.getModel().getSize() > 0) {
153                    selectionList.ensureIndexIsVisible(0);
154                }
155            } else {
156                selectionList.setSelectedIndex(idx);
157                selectionList.ensureIndexIsVisible(idx);
158            }
159        }
160    }
161
162    /**
163     * Comparator that compares the number part of the code numerically.
164     */
165    public static class CodeComparator implements Comparator<String>, Serializable {
166        private static final long serialVersionUID = 1L;
167        private final Pattern codePattern = Pattern.compile("([a-zA-Z]+):(\\d+)");
168
169        @Override
170        public int compare(String c1, String c2) {
171            Matcher matcher1 = codePattern.matcher(c1);
172            Matcher matcher2 = codePattern.matcher(c2);
173            if (matcher1.matches()) {
174                if (matcher2.matches()) {
175                    int cmp1 = matcher1.group(1).compareTo(matcher2.group(1));
176                    if (cmp1 != 0)
177                        return cmp1;
178                    int num1 = Integer.parseInt(matcher1.group(2));
179                    int num2 = Integer.parseInt(matcher2.group(2));
180                    return Integer.compare(num1, num2);
181                } else
182                    return -1;
183            } else if (matcher2.matches())
184                return 1;
185            return c1.compareTo(c2);
186        }
187    }
188
189    @Override
190    public Projection getProjection() {
191        return Projections.getProjectionByCode(code);
192    }
193
194    @Override
195    public String getCurrentCode() {
196        // not needed - getProjection() is overridden
197        throw new UnsupportedOperationException();
198    }
199
200    @Override
201    public String getProjectionName() {
202        // not needed - getProjection() is overridden
203        throw new UnsupportedOperationException();
204    }
205
206    @Override
207    public void setPreferences(Collection<String> args) {
208        if (args != null && !args.isEmpty()) {
209            code = args.iterator().next();
210        }
211    }
212
213    @Override
214    public JPanel getPreferencePanel(ActionListener listener) {
215        return new CodeSelectionPanel(code, listener);
216    }
217
218    @Override
219    public Collection<String> getPreferences(JPanel panel) {
220        if (!(panel instanceof CodeSelectionPanel)) {
221            throw new IllegalArgumentException("Unsupported panel: "+panel);
222        }
223        CodeSelectionPanel csPanel = (CodeSelectionPanel) panel;
224        return Collections.singleton(csPanel.getCode());
225    }
226
227    /* don't return all possible codes - this projection choice it too generic */
228    @Override
229    public String[] allCodes() {
230        return new String[0];
231    }
232
233    /* not needed since allCodes() returns empty array */
234    @Override
235    public Collection<String> getPreferencesFromCode(String code) {
236        return null;
237    }
238
239    @Override
240    public boolean showProjectionCode() {
241        return true;
242    }
243
244    @Override
245    public boolean showProjectionName() {
246        return true;
247    }
248
249}