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