Ticket #13408: patch-projection-custom-cleanup.patch

File patch-projection-custom-cleanup.patch, 18.9 KB (added by michael2402, 8 years ago)
  • src/org/openstreetmap/josm/data/projection/CustomProjection.java

    diff --git a/src/org/openstreetmap/josm/data/projection/CustomProjection.java b/src/org/openstreetmap/josm/data/projection/CustomProjection.java
    index 6574b0f..fdd4d28 100644
    a b package org.openstreetmap.josm.data.projection;  
    44import static org.openstreetmap.josm.tools.I18n.tr;
    55
    66import java.util.ArrayList;
     7import java.util.Arrays;
    78import java.util.EnumMap;
    89import java.util.HashMap;
    910import java.util.List;
    import org.openstreetmap.josm.data.projection.proj.Mercator;  
    3132import org.openstreetmap.josm.data.projection.proj.Proj;
    3233import org.openstreetmap.josm.data.projection.proj.ProjParameters;
    3334import org.openstreetmap.josm.tools.Utils;
     35import org.openstreetmap.josm.tools.bugreport.BugReport;
    3436
    3537/**
    3638 * Custom projection.
    public class CustomProjection extends AbstractProjection {  
    6365    private double metersPerUnitWMTS;
    6466    private String axis = "enu"; // default axis orientation is East, North, Up
    6567
     68    private final static List<String> LON_LAT_VALUES = Arrays.asList("longlat", "latlon", "latlong");
     69
    6670    /**
    6771     * Proj4-like projection parameters. See <a href="https://trac.osgeo.org/proj/wiki/GenParms">reference</a>.
    6872     * @since 7370 (public)
    6973     */
    7074    public enum Param {
    71 
    7275        /** False easting */
    7376        x_0("x_0", true),
    7477        /** False northing */
    public class CustomProjection extends AbstractProjection {  
    154157            for (Param p : Param.values()) {
    155158                paramsByKey.put(p.key, p);
    156159            }
     160            // alias
     161            paramsByKey.put("k", Param.k_0);
    157162        }
    158163
    159164        Param(String key, boolean hasValue) {
    public class CustomProjection extends AbstractProjection {  
    162167        }
    163168    }
    164169
    165     private enum Polarity { NORTH, SOUTH }
     170    private enum Polarity {
     171        NORTH(LatLon.NORTH_POLE),
     172        SOUTH(LatLon.SOUTH_POLE);
    166173
    167     private EnumMap<Polarity, EastNorth> polesEN;
    168     private EnumMap<Polarity, LatLon> polesLL;
    169     {
    170         polesLL = new EnumMap<>(Polarity.class);
    171         polesLL.put(Polarity.NORTH, LatLon.NORTH_POLE);
    172         polesLL.put(Polarity.SOUTH, LatLon.SOUTH_POLE);
     174        private final LatLon latlon;
     175
     176        Polarity(LatLon latlon) {
     177            this.latlon = latlon;
     178        }
     179
     180        private LatLon getLatLon() {
     181            return latlon;
     182        }
    173183    }
    174184
     185    private EnumMap<Polarity, EastNorth> polesEN;
     186
    175187    /**
    176188     * Constructs a new empty {@code CustomProjection}.
    177189     */
    public class CustomProjection extends AbstractProjection {  
    208220            try {
    209221                update(null);
    210222            } catch (ProjectionConfigurationException ex1) {
    211                 throw new RuntimeException(ex1);
     223                throw BugReport.intercept(ex1).put("name", name).put("code", code).put("pref", pref);
    212224            }
    213225        }
    214226    }
    public class CustomProjection extends AbstractProjection {  
    239251            // "utm" is a shortcut for a set of parameters
    240252            if ("utm".equals(parameters.get(Param.proj.key))) {
    241253                String zoneStr = parameters.get(Param.zone.key);
    242                 Integer zone;
    243254                if (zoneStr == null)
    244255                    throw new ProjectionConfigurationException(tr("UTM projection (''+proj=utm'') requires ''+zone=...'' parameter."));
     256                Integer zone;
    245257                try {
    246258                    zone = Integer.valueOf(zoneStr);
    247259                } catch (NumberFormatException e) {
    public class CustomProjection extends AbstractProjection {  
    331343     */
    332344    public static Map<String, String> parseParameterList(String pref, boolean ignoreUnknownParameter) throws ProjectionConfigurationException {
    333345        Map<String, String> parameters = new HashMap<>();
    334         String[] parts = Utils.WHITE_SPACES_PATTERN.split(pref.trim());
    335346        if (pref.trim().isEmpty()) {
    336             parts = new String[0];
     347            return parameters;
    337348        }
     349
     350        Pattern keyPattern = Pattern.compile("\\+(?<key>[a-zA-Z0-9_]+)(=(?<value>.*))?");
     351        String[] parts = Utils.WHITE_SPACES_PATTERN.split(pref.trim());
    338352        for (String part : parts) {
    339             if (part.isEmpty() || part.charAt(0) != '+')
    340                 throw new ProjectionConfigurationException(tr("Parameter must begin with a ''+'' character (found ''{0}'')", part));
    341             Matcher m = Pattern.compile("\\+([a-zA-Z0-9_]+)(=(.*))?").matcher(part);
     353            Matcher m = keyPattern.matcher(part);
    342354            if (m.matches()) {
    343                 String key = m.group(1);
    344                 // alias
    345                 if ("k".equals(key)) {
    346                     key = Param.k_0.key;
    347                 }
    348                 String value = null;
    349                 if (m.groupCount() >= 3) {
    350                     value = m.group(3);
    351                     // some aliases
    352                     if (key.equals(Param.proj.key)) {
    353                         if ("longlat".equals(value) || "latlon".equals(value) || "latlong".equals(value)) {
    354                             value = "lonlat";
    355                         }
    356                     }
     355                String key = m.group("key");
     356                String value = m.group("value");
     357                // some aliases
     358                if (key.equals(Param.proj.key) && LON_LAT_VALUES.contains(value)) {
     359                    value = "lonlat";
    357360                }
    358                 if (!Param.paramsByKey.containsKey(key)) {
     361                Param param = Param.paramsByKey.get(key);
     362                if (param == null) {
    359363                    if (!ignoreUnknownParameter)
    360364                        throw new ProjectionConfigurationException(tr("Unknown parameter: ''{0}''.", key));
    361365                } else {
    362                     if (Param.paramsByKey.get(key).hasValue && value == null)
     366                    if (param.hasValue && value == null)
    363367                        throw new ProjectionConfigurationException(tr("Value expected for parameter ''{0}''.", key));
    364                     if (!Param.paramsByKey.get(key).hasValue && value != null)
     368                    if (!param.hasValue && value != null)
    365369                        throw new ProjectionConfigurationException(tr("No value expected for parameter ''{0}''.", key));
     370                    key = param.key; // To be really sure, we might have an alias.
    366371                }
    367372                parameters.put(key, value);
    368             } else
     373            } else if (!part.startsWith("+")) {
     374                    throw new ProjectionConfigurationException(tr("Parameter must begin with a ''+'' character (found ''{0}'')", part));
     375            } else {
    369376                throw new ProjectionConfigurationException(tr("Unexpected parameter format (''{0}'')", part));
     377            }
    370378        }
    371379        return parameters;
    372380    }
    public class CustomProjection extends AbstractProjection {  
    400408        return parameters;
    401409    }
    402410
     411    /**
     412     * Gets the ellipsoid
     413     * @param parameters The parameters to get the value from
     414     * @return The Ellipsoid as specified with the parameters
     415     * @throws ProjectionConfigurationException
     416     */
    403417    public Ellipsoid parseEllipsoid(Map<String, String> parameters) throws ProjectionConfigurationException {
    404418        String code = parameters.get(Param.ellps.key);
    405419        if (code != null) {
    public class CustomProjection extends AbstractProjection {  
    439453        return null;
    440454    }
    441455
     456    /**
     457     * Gets the datum
     458     * @param parameters The parameters to get the value from
     459     * @param ellps The ellisoid that was previously computed
     460     * @return The Datum as specified with the parameters
     461     * @throws ProjectionConfigurationException
     462     */
    442463    public Datum parseDatum(Map<String, String> parameters, Ellipsoid ellps) throws ProjectionConfigurationException {
    443464        String datumId = parameters.get(Param.datum.key);
    444465        if (datumId != null) {
    public class CustomProjection extends AbstractProjection {  
    518539                    towgs84Param.get(6));
    519540    }
    520541
     542    /**
     543     * Gets a projection using the given ellipsoid
     544     * @param parameters Additional parameters
     545     * @param ellps The {@link Ellipsoid}
     546     * @return The projection
     547     * @throws ProjectionConfigurationException
     548     */
    521549    public Proj parseProjection(Map<String, String> parameters, Ellipsoid ellps) throws ProjectionConfigurationException {
    522550        String id = parameters.get(Param.proj.key);
    523551        if (id == null) throw new ProjectionConfigurationException(tr("Projection required (+proj=*)"));
    public class CustomProjection extends AbstractProjection {  
    577605        return proj;
    578606    }
    579607
     608    /**
     609     * Converts a string to a bounds object
     610     * @param boundsStr The string as comma separated list of angles.
     611     * @return The bounds.
     612     * @throws ProjectionConfigurationException
     613     * @see {@link CustomProjection#parseAngle(String, String)}
     614     */
    580615    public static Bounds parseBounds(String boundsStr) throws ProjectionConfigurationException {
    581616        String[] numStr = boundsStr.split(",");
    582617        if (numStr.length != 4)
    public class CustomProjection extends AbstractProjection {  
    606641        }
    607642    }
    608643
     644    /**
     645     * Convert an angle string to a double value
     646     * @param angleStr The string. e.g. -1.1 or 50d 10' 3"
     647     * @param parameterName Only for error message.
     648     * @return The angle value, in degrees.
     649     * @throws ProjectionConfigurationException
     650     */
    609651    public static double parseAngle(String angleStr, String parameterName) throws ProjectionConfigurationException {
    610         String s = angleStr;
     652        final String floatPattern = "(\\d+(\\.\\d*)?)";
     653        // pattern does all error handling.
     654        Matcher in = Pattern.compile("^(?<neg1>-)?"
     655                + "(?=\\d)(?:(?<single>" + floatPattern + ")|"
     656                + "((?<degree>" + floatPattern + ")d)?"
     657                + "((?<minutes>" + floatPattern + ")\')?"
     658                + "((?<seconds>" + floatPattern + ")\")?)"
     659                + "(?:[NE]|(?<neg2>[SW]))?$").matcher(angleStr);
     660
     661        if (!in.find()) {
     662            throw new ProjectionConfigurationException(
     663                    tr("Unable to parse value ''{1}'' of parameter ''{0}'' as coordinate value.", parameterName, angleStr));
     664        }
     665
    611666        double value = 0;
    612         boolean neg = false;
    613         Matcher m = Pattern.compile("^-").matcher(s);
    614         if (m.find()) {
    615             neg = true;
    616             s = s.substring(m.end());
     667        if (in.group("single") != null) {
     668            value += Double.parseDouble(in.group("single"));
    617669        }
    618         final String floatPattern = "(\\d+(\\.\\d*)?)";
    619         boolean dms = false;
    620         double deg = 0.0, min = 0.0, sec = 0.0;
    621         // degrees
    622         m = Pattern.compile("^"+floatPattern+"d").matcher(s);
    623         if (m.find()) {
    624             s = s.substring(m.end());
    625             deg = Double.parseDouble(m.group(1));
    626             dms = true;
    627         }
    628         // minutes
    629         m = Pattern.compile("^"+floatPattern+"'").matcher(s);
    630         if (m.find()) {
    631             s = s.substring(m.end());
    632             min = Double.parseDouble(m.group(1));
    633             dms = true;
    634         }
    635         // seconds
    636         m = Pattern.compile("^"+floatPattern+"\"").matcher(s);
    637         if (m.find()) {
    638             s = s.substring(m.end());
    639             sec = Double.parseDouble(m.group(1));
    640             dms = true;
    641         }
    642         // plain number (in degrees)
    643         if (dms) {
    644             value = deg + (min/60.0) + (sec/3600.0);
    645         } else {
    646             m = Pattern.compile("^"+floatPattern).matcher(s);
    647             if (m.find()) {
    648                 s = s.substring(m.end());
    649                 value += Double.parseDouble(m.group(1));
    650             }
     670        if (in.group("degree") != null) {
     671            value += Double.parseDouble(in.group("degree"));
    651672        }
    652         m = Pattern.compile("^(N|E)", Pattern.CASE_INSENSITIVE).matcher(s);
    653         if (m.find()) {
    654             s = s.substring(m.end());
    655         } else {
    656             m = Pattern.compile("^(S|W)", Pattern.CASE_INSENSITIVE).matcher(s);
    657             if (m.find()) {
    658                 s = s.substring(m.end());
    659                 neg = !neg;
    660             }
     673        if (in.group("minutes") != null) {
     674            value += Double.parseDouble(in.group("minutes")) / 60;
    661675        }
    662         if (neg) {
    663             value = -value;
     676        if (in.group("seconds") != null) {
     677            value += Double.parseDouble(in.group("seconds")) / 3600;
    664678        }
    665         if (!s.isEmpty()) {
    666             throw new ProjectionConfigurationException(
    667                     tr("Unable to parse value ''{1}'' of parameter ''{0}'' as coordinate value.", parameterName, angleStr));
     679
     680        if (in.group("neg1") != null ^ in.group("neg2") != null) {
     681            value = -value;
    668682        }
    669683        return value;
    670684    }
    public class CustomProjection extends AbstractProjection {  
    683697
    684698    @Override
    685699    public String toCode() {
    686         return code != null ? code : "proj:" + (pref == null ? "ERROR" : pref);
     700        if (code != null) {
     701            return code;
     702        } else if (pref != null) {
     703            return "proj:" + pref;
     704        } else {
     705            return "proj:ERROR";
     706        }
    687707    }
    688708
    689709    @Override
    690710    public String getCacheDirectoryName() {
    691         return cacheDir != null ? cacheDir : "proj-"+Utils.md5Hex(pref == null ? "" : pref).substring(0, 4);
     711        if (cacheDir != null) {
     712            return cacheDir;
     713        } else {
     714            return "proj-" + Utils.md5Hex(pref == null ? "" : pref).substring(0, 4);
     715        }
    692716    }
    693717
    694718    @Override
    695719    public Bounds getWorldBoundsLatLon() {
    696         if (bounds != null) return bounds;
    697         Bounds ab = proj.getAlgorithmBounds();
    698         if (ab != null) {
    699             double minlon = Math.max(ab.getMinLon() + lon0 + pm, -180);
    700             double maxlon = Math.min(ab.getMaxLon() + lon0 + pm, 180);
    701             return new Bounds(ab.getMinLat(), minlon, ab.getMaxLat(), maxlon, false);
    702         } else {
    703             return new Bounds(
    704                 new LatLon(-90.0, -180.0),
    705                 new LatLon(90.0, 180.0));
     720        if (bounds == null) {
     721            Bounds ab = proj.getAlgorithmBounds();
     722            if (ab != null) {
     723                double minlon = Math.max(ab.getMinLon() + lon0 + pm, -180);
     724                double maxlon = Math.min(ab.getMaxLon() + lon0 + pm, 180);
     725                bounds = new Bounds(ab.getMinLat(), minlon, ab.getMaxLat(), maxlon, false);
     726            } else {
     727                bounds = new Bounds(
     728                    new LatLon(-90.0, -180.0),
     729                    new LatLon(90.0, 180.0));
     730            }
    706731        }
     732        return bounds;
    707733    }
    708734
    709735    @Override
    public class CustomProjection extends AbstractProjection {  
    804830            polesEN = new EnumMap<>(Polarity.class);
    805831            for (Polarity p : Polarity.values()) {
    806832                polesEN.put(p, null);
    807                 LatLon ll = polesLL.get(p);
     833                LatLon ll = p.getLatLon();
    808834                try {
    809835                    EastNorth enPole = latlon2eastNorth(ll);
    810836                    if (enPole.isValid()) {
    public class CustomProjection extends AbstractProjection {  
    852878        for (Polarity p : Polarity.values()) {
    853879            EastNorth pole = getPole(p);
    854880            if (pole != null && r.contains(pole)) {
    855                 result.extend(polesLL.get(p));
     881                result.extend(p.getLatLon());
    856882            }
    857883        }
    858884        return result;
  • new file test/unit/org/openstreetmap/josm/data/projection/CustomProjectionTest.java

    diff --git a/test/unit/org/openstreetmap/josm/data/projection/CustomProjectionTest.java b/test/unit/org/openstreetmap/josm/data/projection/CustomProjectionTest.java
    new file mode 100644
    index 0000000..3b2ae69
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.projection;
     3
     4import static org.junit.Assert.assertEquals;
     5import static org.junit.Assert.assertTrue;
     6import static org.junit.Assert.fail;
     7
     8import java.util.stream.Stream;
     9
     10import org.junit.Rule;
     11import org.junit.Test;
     12import org.openstreetmap.josm.testutils.JOSMTestRules;
     13
     14import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
     15
     16/**
     17 * Tests for {@link CustomProjection}
     18 * @author Michael Zangl
     19 * @since xxx
     20 */
     21public class CustomProjectionTest {
     22    /**
     23     * Need pref to load pref.
     24     */
     25    @Rule
     26    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
     27    public JOSMTestRules test = new JOSMTestRules().preferences();
     28
     29    /**
     30     * Test {@link CustomProjection#parseAngle(String, String)}
     31     * @throws ProjectionConfigurationException
     32     */
     33    @Test
     34    public void testParseAngle() throws ProjectionConfigurationException {
     35        assertEquals(0, CustomProjection.parseAngle("0", "xxx"), 1e-10);
     36        assertEquals(1, CustomProjection.parseAngle("1", "xxx"), 1e-10);
     37        assertEquals(1.1, CustomProjection.parseAngle("1.1", "xxx"), 1e-10);
     38
     39        assertEquals(1, CustomProjection.parseAngle("1d", "xxx"), 1e-10);
     40        assertEquals(1.1, CustomProjection.parseAngle("1.1d", "xxx"), 1e-10);
     41
     42        assertEquals(1 / 60.0, CustomProjection.parseAngle("1'", "xxx"), 1e-10);
     43        assertEquals(1.1 / 60.0, CustomProjection.parseAngle("1.1'", "xxx"), 1e-10);
     44
     45        assertEquals(1 / 3600.0, CustomProjection.parseAngle("1\"", "xxx"), 1e-10);
     46        assertEquals(1.1 / 3600.0, CustomProjection.parseAngle("1.1\"", "xxx"), 1e-10);
     47
     48        // negate
     49        assertEquals(-1.1, CustomProjection.parseAngle("-1.1", "xxx"), 1e-10);
     50        assertEquals(1.1, CustomProjection.parseAngle("1.1N", "xxx"), 1e-10);
     51        assertEquals(1.1, CustomProjection.parseAngle("1.1E", "xxx"), 1e-10);
     52        assertEquals(-1.1, CustomProjection.parseAngle("1.1S", "xxx"), 1e-10);
     53        assertEquals(-1.1, CustomProjection.parseAngle("1.1W", "xxx"), 1e-10);
     54        assertEquals(-1.1, CustomProjection.parseAngle("-1.1N", "xxx"), 1e-10);
     55        assertEquals(-1.1, CustomProjection.parseAngle("-1.1E", "xxx"), 1e-10);
     56        assertEquals(1.1, CustomProjection.parseAngle("-1.1S", "xxx"), 1e-10);
     57        assertEquals(1.1, CustomProjection.parseAngle("-1.1W", "xxx"), 1e-10);
     58
     59        // combine
     60        assertEquals(1.1 + 3 / 60.0 + 5.2 / 3600.0, CustomProjection.parseAngle("1.1d3'5.2\"", "xxx"), 1e-10);
     61
     62        // fail
     63        Stream.of("", "-", "-N", "N", "1.1 ", "x", "1.1d1.1d", "1.1e","1.1.1",".1", "1.1d3\"5.2'").forEach(
     64                s -> {
     65                    try {
     66                        CustomProjection.parseAngle(s, "xxxx");
     67                        fail("Expected exception for " + s);
     68                    } catch (ProjectionConfigurationException e) {
     69                        // good!
     70                        assertTrue(e.getMessage().contains("xxx"));
     71                    }
     72                });
     73    }
     74}