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}