001    // License: GPL. For details, see LICENSE file.
002    package org.openstreetmap.josm.data.projection;
003    
004    import static org.openstreetmap.josm.tools.I18n.tr;
005    
006    import java.util.ArrayList;
007    import java.util.HashMap;
008    import java.util.List;
009    import java.util.Map;
010    import java.util.regex.Matcher;
011    import java.util.regex.Pattern;
012    
013    import org.openstreetmap.josm.data.Bounds;
014    import org.openstreetmap.josm.data.coor.LatLon;
015    import org.openstreetmap.josm.data.projection.datum.CentricDatum;
016    import org.openstreetmap.josm.data.projection.datum.Datum;
017    import org.openstreetmap.josm.data.projection.datum.NTV2Datum;
018    import org.openstreetmap.josm.data.projection.datum.NTV2GridShiftFileWrapper;
019    import org.openstreetmap.josm.data.projection.datum.NullDatum;
020    import org.openstreetmap.josm.data.projection.datum.SevenParameterDatum;
021    import org.openstreetmap.josm.data.projection.datum.ThreeParameterDatum;
022    import org.openstreetmap.josm.data.projection.datum.WGS84Datum;
023    import org.openstreetmap.josm.data.projection.proj.Mercator;
024    import org.openstreetmap.josm.data.projection.proj.Proj;
025    import org.openstreetmap.josm.data.projection.proj.ProjParameters;
026    import org.openstreetmap.josm.tools.Utils;
027    
028    /**
029     * Custom projection
030     *
031     * Inspired by PROJ.4 and Proj4J.
032     */
033    public class CustomProjection extends AbstractProjection {
034    
035        /**
036         * pref String that defines the projection
037         *
038         * null means fall back mode (Mercator)
039         */
040        protected String pref;
041        protected String name;
042        protected String code;
043        protected String cacheDir;
044        protected Bounds bounds;
045    
046        protected static enum Param {
047    
048            x_0("x_0", true),
049            y_0("y_0", true),
050            lon_0("lon_0", true),
051            k_0("k_0", true),
052            ellps("ellps", true),
053            a("a", true),
054            es("es", true),
055            rf("rf", true),
056            f("f", true),
057            b("b", true),
058            datum("datum", true),
059            towgs84("towgs84", true),
060            nadgrids("nadgrids", true),
061            proj("proj", true),
062            lat_0("lat_0", true),
063            lat_1("lat_1", true),
064            lat_2("lat_2", true),
065            wktext("wktext", false),  // ignored
066            units("units", true),     // ignored
067            no_defs("no_defs", false),
068            init("init", true),
069            // JOSM extension, not present in PROJ.4
070            bounds("bounds", true);
071    
072            public String key;
073            public boolean hasValue;
074    
075            public final static Map<String, Param> paramsByKey = new HashMap<String, Param>();
076            static {
077                for (Param p : Param.values()) {
078                    paramsByKey.put(p.key, p);
079                }
080            }
081    
082            Param(String key, boolean hasValue) {
083                this.key = key;
084                this.hasValue = hasValue;
085            }
086        }
087    
088        public CustomProjection() {
089        }
090    
091        public CustomProjection(String pref) {
092            this(null, null, pref, null);
093        }
094    
095        /**
096         * Constructor.
097         *
098         * @param name describe projection in one or two words
099         * @param code unique code for this projection - may be null
100         * @param pref the string that defines the custom projection
101         * @param cacheDir cache directory name
102         */
103        public CustomProjection(String name, String code, String pref, String cacheDir) {
104            this.name = name;
105            this.code = code;
106            this.pref = pref;
107            this.cacheDir = cacheDir;
108            try {
109                update(pref);
110            } catch (ProjectionConfigurationException ex) {
111                try {
112                    update(null);
113                } catch (ProjectionConfigurationException ex1) {
114                    throw new RuntimeException();
115                }
116            }
117        }
118    
119        public void update(String pref) throws ProjectionConfigurationException {
120            this.pref = pref;
121            if (pref == null) {
122                ellps = Ellipsoid.WGS84;
123                datum = WGS84Datum.INSTANCE;
124                proj = new Mercator();
125                bounds = new Bounds(
126                        new LatLon(-85.05112877980659, -180.0),
127                        new LatLon(85.05112877980659, 180.0), true);
128            } else {
129                Map<String, String> parameters = parseParameterList(pref);
130                ellps = parseEllipsoid(parameters);
131                datum = parseDatum(parameters, ellps);
132                proj = parseProjection(parameters, ellps);
133                String s = parameters.get(Param.x_0.key);
134                if (s != null) {
135                    this.x_0 = parseDouble(s, Param.x_0.key);
136                }
137                s = parameters.get(Param.y_0.key);
138                if (s != null) {
139                    this.y_0 = parseDouble(s, Param.y_0.key);
140                }
141                s = parameters.get(Param.lon_0.key);
142                if (s != null) {
143                    this.lon_0 = parseAngle(s, Param.lon_0.key);
144                }
145                s = parameters.get(Param.k_0.key);
146                if (s != null) {
147                    this.k_0 = parseDouble(s, Param.k_0.key);
148                }
149                s = parameters.get(Param.bounds.key);
150                if (s != null) {
151                    this.bounds = parseBounds(s);
152                }
153            }
154        }
155    
156        private Map<String, String> parseParameterList(String pref) throws ProjectionConfigurationException {
157            Map<String, String> parameters = new HashMap<String, String>();
158            String[] parts = pref.trim().split("\\s+");
159            if (pref.trim().isEmpty()) {
160                parts = new String[0];
161            }
162            for (int i = 0; i < parts.length; i++) {
163                String part = parts[i];
164                if (part.isEmpty() || part.charAt(0) != '+')
165                    throw new ProjectionConfigurationException(tr("Parameter must begin with a ''+'' character (found ''{0}'')", part));
166                Matcher m = Pattern.compile("\\+([a-zA-Z0-9_]+)(=(.*))?").matcher(part);
167                if (m.matches()) {
168                    String key = m.group(1);
169                    // alias
170                    if (key.equals("k")) {
171                        key = Param.k_0.key;
172                    }
173                    String value = null;
174                    if (m.groupCount() >= 3) {
175                        value = m.group(3);
176                        // same aliases
177                        if (key.equals(Param.proj.key)) {
178                            if (value.equals("longlat") || value.equals("latlon") || value.equals("latlong")) {
179                                value = "lonlat";
180                            }
181                        }
182                    }
183                    if (!Param.paramsByKey.containsKey(key))
184                        throw new ProjectionConfigurationException(tr("Unkown parameter: ''{0}''.", key));
185                    if (Param.paramsByKey.get(key).hasValue && value == null)
186                        throw new ProjectionConfigurationException(tr("Value expected for parameter ''{0}''.", key));
187                    if (!Param.paramsByKey.get(key).hasValue && value != null)
188                        throw new ProjectionConfigurationException(tr("No value expected for parameter ''{0}''.", key));
189                    parameters.put(key, value);
190                } else
191                    throw new ProjectionConfigurationException(tr("Unexpected parameter format (''{0}'')", part));
192            }
193            // recursive resolution of +init includes
194            String initKey = parameters.get(Param.init.key);
195            if (initKey != null) {
196                String init = Projections.getInit(initKey);
197                if (init == null)
198                    throw new ProjectionConfigurationException(tr("Value ''{0}'' for option +init not supported.", initKey));
199                Map<String, String> initp = null;
200                try {
201                    initp = parseParameterList(init);
202                } catch (ProjectionConfigurationException ex) {
203                    throw new ProjectionConfigurationException(tr(initKey+": "+ex.getMessage()));
204                }
205                for (Map.Entry<String, String> e : parameters.entrySet()) {
206                    initp.put(e.getKey(), e.getValue());
207                }
208                return initp;
209            }
210            return parameters;
211        }
212    
213        public Ellipsoid parseEllipsoid(Map<String, String> parameters) throws ProjectionConfigurationException {
214            String code = parameters.get(Param.ellps.key);
215            if (code != null) {
216                Ellipsoid ellipsoid = Projections.getEllipsoid(code);
217                if (ellipsoid == null) {
218                    throw new ProjectionConfigurationException(tr("Ellipsoid ''{0}'' not supported.", code));
219                } else {
220                    return ellipsoid;
221                }
222            }
223            String s = parameters.get(Param.a.key);
224            if (s != null) {
225                double a = parseDouble(s, Param.a.key);
226                if (parameters.get(Param.es.key) != null) {
227                    double es = parseDouble(parameters, Param.es.key);
228                    return Ellipsoid.create_a_es(a, es);
229                }
230                if (parameters.get(Param.rf.key) != null) {
231                    double rf = parseDouble(parameters, Param.rf.key);
232                    return Ellipsoid.create_a_rf(a, rf);
233                }
234                if (parameters.get(Param.f.key) != null) {
235                    double f = parseDouble(parameters, Param.f.key);
236                    return Ellipsoid.create_a_f(a, f);
237                }
238                if (parameters.get(Param.b.key) != null) {
239                    double b = parseDouble(parameters, Param.b.key);
240                    return Ellipsoid.create_a_b(a, b);
241                }
242            }
243            if (parameters.containsKey(Param.a.key) ||
244                    parameters.containsKey(Param.es.key) ||
245                    parameters.containsKey(Param.rf.key) ||
246                    parameters.containsKey(Param.f.key) ||
247                    parameters.containsKey(Param.b.key))
248                throw new ProjectionConfigurationException(tr("Combination of ellipsoid parameters is not supported."));
249            if (parameters.containsKey(Param.no_defs.key))
250                throw new ProjectionConfigurationException(tr("Ellipsoid required (+ellps=* or +a=*, +b=*)"));
251            // nothing specified, use WGS84 as default
252            return Ellipsoid.WGS84;
253        }
254    
255        public Datum parseDatum(Map<String, String> parameters, Ellipsoid ellps) throws ProjectionConfigurationException {
256            String nadgridsId = parameters.get(Param.nadgrids.key);
257            if (nadgridsId != null) {
258                if (nadgridsId.startsWith("@")) {
259                    nadgridsId = nadgridsId.substring(1);
260                }
261                if (nadgridsId.equals("null"))
262                    return new NullDatum(null, ellps);
263                NTV2GridShiftFileWrapper nadgrids = Projections.getNTV2Grid(nadgridsId);
264                if (nadgrids == null)
265                    throw new ProjectionConfigurationException(tr("Grid shift file ''{0}'' for option +nadgrids not supported.", nadgridsId));
266                return new NTV2Datum(nadgridsId, null, ellps, nadgrids);
267            }
268    
269            String towgs84 = parameters.get(Param.towgs84.key);
270            if (towgs84 != null)
271                return parseToWGS84(towgs84, ellps);
272    
273            String datumId = parameters.get(Param.datum.key);
274            if (datumId != null) {
275                Datum datum = Projections.getDatum(datumId);
276                if (datum == null) throw new ProjectionConfigurationException(tr("Unkown datum identifier: ''{0}''", datumId));
277                return datum;
278            }
279            if (parameters.containsKey(Param.no_defs.key))
280                throw new ProjectionConfigurationException(tr("Datum required (+datum=*, +towgs84=* or +nadgirds=*)"));
281            return new CentricDatum(null, null, ellps);
282        }
283    
284        public Datum parseToWGS84(String paramList, Ellipsoid ellps) throws ProjectionConfigurationException {
285            String[] numStr = paramList.split(",");
286    
287            if (numStr.length != 3 && numStr.length != 7)
288                throw new ProjectionConfigurationException(tr("Unexpected number of arguments for parameter ''towgs84'' (must be 3 or 7)"));
289            List<Double> towgs84Param = new ArrayList<Double>();
290            for (int i = 0; i < numStr.length; i++) {
291                try {
292                    towgs84Param.add(Double.parseDouble(numStr[i]));
293                } catch (NumberFormatException e) {
294                    throw new ProjectionConfigurationException(tr("Unable to parse value of parameter ''towgs84'' (''{0}'')", numStr[i]));
295                }
296            }
297            boolean isCentric = true;
298            for (int i = 0; i<towgs84Param.size(); i++) {
299                if (towgs84Param.get(i) != 0.0) {
300                    isCentric = false;
301                    break;
302                }
303            }
304            if (isCentric)
305                return new CentricDatum(null, null, ellps);
306            boolean is3Param = true;
307            for (int i = 3; i<towgs84Param.size(); i++) {
308                if (towgs84Param.get(i) != 0.0) {
309                    is3Param = false;
310                    break;
311                }
312            }
313            if (is3Param)
314                return new ThreeParameterDatum(null, null, ellps,
315                        towgs84Param.get(0),
316                        towgs84Param.get(1),
317                        towgs84Param.get(2));
318            else
319                return new SevenParameterDatum(null, null, ellps,
320                        towgs84Param.get(0),
321                        towgs84Param.get(1),
322                        towgs84Param.get(2),
323                        towgs84Param.get(3),
324                        towgs84Param.get(4),
325                        towgs84Param.get(5),
326                        towgs84Param.get(6));
327        }
328    
329        public Proj parseProjection(Map<String, String> parameters, Ellipsoid ellps) throws ProjectionConfigurationException {
330            String id = (String) parameters.get(Param.proj.key);
331            if (id == null) throw new ProjectionConfigurationException(tr("Projection required (+proj=*)"));
332    
333            Proj proj =  Projections.getBaseProjection(id);
334            if (proj == null) throw new ProjectionConfigurationException(tr("Unkown projection identifier: ''{0}''", id));
335    
336            ProjParameters projParams = new ProjParameters();
337    
338            projParams.ellps = ellps;
339    
340            String s;
341            s = parameters.get(Param.lat_0.key);
342            if (s != null) {
343                projParams.lat_0 = parseAngle(s, Param.lat_0.key);
344            }
345            s = parameters.get(Param.lat_1.key);
346            if (s != null) {
347                projParams.lat_1 = parseAngle(s, Param.lat_1.key);
348            }
349            s = parameters.get(Param.lat_2.key);
350            if (s != null) {
351                projParams.lat_2 = parseAngle(s, Param.lat_2.key);
352            }
353            proj.initialize(projParams);
354            return proj;
355        }
356    
357        public static Bounds parseBounds(String boundsStr) throws ProjectionConfigurationException {
358            String[] numStr = boundsStr.split(",");
359            if (numStr.length != 4)
360                throw new ProjectionConfigurationException(tr("Unexpected number of arguments for parameter ''+bounds'' (must be 4)"));
361            return new Bounds(parseAngle(numStr[1], "minlat (+bounds)"),
362                    parseAngle(numStr[0], "minlon (+bounds)"),
363                    parseAngle(numStr[3], "maxlat (+bounds)"),
364                    parseAngle(numStr[2], "maxlon (+bounds)"), false);
365        }
366    
367        public static double parseDouble(Map<String, String> parameters, String parameterName) throws ProjectionConfigurationException {
368            String doubleStr = parameters.get(parameterName);
369            if (doubleStr == null && parameters.containsKey(parameterName))
370                throw new ProjectionConfigurationException(
371                        tr("Expected number argument for parameter ''{0}''", parameterName));
372            return parseDouble(doubleStr, parameterName);
373        }
374    
375        public static double parseDouble(String doubleStr, String parameterName) throws ProjectionConfigurationException {
376            try {
377                return Double.parseDouble(doubleStr);
378            } catch (NumberFormatException e) {
379                throw new ProjectionConfigurationException(
380                        tr("Unable to parse value ''{1}'' of parameter ''{0}'' as number.", parameterName, doubleStr));
381            }
382        }
383    
384        public static double parseAngle(String angleStr, String parameterName) throws ProjectionConfigurationException {
385            String s = angleStr;
386            double value = 0;
387            boolean neg = false;
388            Matcher m = Pattern.compile("^-").matcher(s);
389            if (m.find()) {
390                neg = true;
391                s = s.substring(m.end());
392            }
393            final String FLOAT = "(\\d+(\\.\\d*)?)";
394            boolean dms = false;
395            double deg = 0.0, min = 0.0, sec = 0.0;
396            // degrees
397            m = Pattern.compile("^"+FLOAT+"d").matcher(s);
398            if (m.find()) {
399                s = s.substring(m.end());
400                deg = Double.parseDouble(m.group(1));
401                dms = true;
402            }
403            // minutes
404            m = Pattern.compile("^"+FLOAT+"'").matcher(s);
405            if (m.find()) {
406                s = s.substring(m.end());
407                min = Double.parseDouble(m.group(1));
408                dms = true;
409            }
410            // seconds
411            m = Pattern.compile("^"+FLOAT+"\"").matcher(s);
412            if (m.find()) {
413                s = s.substring(m.end());
414                sec = Double.parseDouble(m.group(1));
415                dms = true;
416            }
417            // plain number (in degrees)
418            if (dms) {
419                value = deg + (min/60.0) + (sec/3600.0);
420            } else {
421                m = Pattern.compile("^"+FLOAT).matcher(s);
422                if (m.find()) {
423                    s = s.substring(m.end());
424                    value += Double.parseDouble(m.group(1));
425                }
426            }
427            m = Pattern.compile("^(N|E)", Pattern.CASE_INSENSITIVE).matcher(s);
428            if (m.find()) {
429                s = s.substring(m.end());
430            } else {
431                m = Pattern.compile("^(S|W)", Pattern.CASE_INSENSITIVE).matcher(s);
432                if (m.find()) {
433                    s = s.substring(m.end());
434                    neg = !neg;
435                }
436            }
437            if (neg) {
438                value = -value;
439            }
440            if (!s.isEmpty()) {
441                throw new ProjectionConfigurationException(
442                        tr("Unable to parse value ''{1}'' of parameter ''{0}'' as coordinate value.", parameterName, angleStr));
443            }
444            return value;
445        }
446    
447        @Override
448        public Integer getEpsgCode() {
449            if (code != null && code.startsWith("EPSG:")) {
450                try {
451                    return Integer.parseInt(code.substring(5));
452                } catch (NumberFormatException e) {}
453            }
454            return null;
455        }
456    
457        @Override
458        public String toCode() {
459            return code != null ? code : "proj:" + (pref == null ? "ERROR" : pref);
460        }
461    
462        @Override
463        public String getCacheDirectoryName() {
464            return cacheDir != null ? cacheDir : "proj-"+Utils.md5Hex(pref == null ? "" : pref).substring(0, 4);
465        }
466    
467        @Override
468        public Bounds getWorldBoundsLatLon() {
469            if (bounds != null) return bounds;
470            return new Bounds(
471                new LatLon(-90.0, -180.0),
472                new LatLon(90.0, 180.0));
473        }
474    
475        @Override
476        public String toString() {
477            return name != null ? name : tr("Custom Projection");
478        }
479    }