source: josm/trunk/src/org/openstreetmap/josm/data/projection/CustomProjection.java@ 9100

Last change on this file since 9100 was 8846, checked in by Don-vip, 9 years ago

sonar - fb-contrib - minor performance improvements:

  • Method passes constant String of length 1 to character overridden method
  • Method needlessly boxes a boolean constant
  • Method uses iterator().next() on a List to get the first item
  • Method converts String to boxed primitive using excessive boxing
  • Method converts String to primitive using excessive boxing
  • Method creates array using constants
  • Class defines List based fields but uses them like Sets
  • Property svn:eol-style set to native
File size: 24.2 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.projection;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.util.ArrayList;
7import java.util.HashMap;
8import java.util.List;
9import java.util.Map;
10import java.util.concurrent.ConcurrentHashMap;
11import java.util.regex.Matcher;
12import java.util.regex.Pattern;
13
14import org.openstreetmap.josm.Main;
15import org.openstreetmap.josm.data.Bounds;
16import org.openstreetmap.josm.data.coor.LatLon;
17import org.openstreetmap.josm.data.projection.datum.CentricDatum;
18import org.openstreetmap.josm.data.projection.datum.Datum;
19import org.openstreetmap.josm.data.projection.datum.NTV2Datum;
20import org.openstreetmap.josm.data.projection.datum.NTV2GridShiftFileWrapper;
21import org.openstreetmap.josm.data.projection.datum.NullDatum;
22import org.openstreetmap.josm.data.projection.datum.SevenParameterDatum;
23import org.openstreetmap.josm.data.projection.datum.ThreeParameterDatum;
24import org.openstreetmap.josm.data.projection.datum.WGS84Datum;
25import org.openstreetmap.josm.data.projection.proj.Mercator;
26import org.openstreetmap.josm.data.projection.proj.Proj;
27import org.openstreetmap.josm.data.projection.proj.ProjParameters;
28import org.openstreetmap.josm.tools.Utils;
29
30/**
31 * Custom projection.
32 *
33 * Inspired by PROJ.4 and Proj4J.
34 * @since 5072
35 */
36public class CustomProjection extends AbstractProjection {
37
38 private static final double METER_PER_UNIT_DEGREE = 2 * Math.PI * 6370997 / 360;
39 private static final Map<String, Double> UNITS_TO_METERS = getUnitsToMeters();
40
41 /**
42 * pref String that defines the projection
43 *
44 * null means fall back mode (Mercator)
45 */
46 protected String pref;
47 protected String name;
48 protected String code;
49 protected String cacheDir;
50 protected Bounds bounds;
51 private double metersPerUnit = METER_PER_UNIT_DEGREE; // default to degrees
52 private String axis = "enu"; // default axis orientation is East, North, Up
53
54 /**
55 * Proj4-like projection parameters. See <a href="https://trac.osgeo.org/proj/wiki/GenParms">reference</a>.
56 * @since 7370 (public)
57 */
58 public enum Param {
59
60 /** False easting */
61 x_0("x_0", true),
62 /** False northing */
63 y_0("y_0", true),
64 /** Central meridian */
65 lon_0("lon_0", true),
66 /** Scaling factor */
67 k_0("k_0", true),
68 /** Ellipsoid name (see {@code proj -le}) */
69 ellps("ellps", true),
70 /** Semimajor radius of the ellipsoid axis */
71 a("a", true),
72 /** Eccentricity of the ellipsoid squared */
73 es("es", true),
74 /** Reciprocal of the ellipsoid flattening term (e.g. 298) */
75 rf("rf", true),
76 /** Flattening of the ellipsoid = 1-sqrt(1-e^2) */
77 f("f", true),
78 /** Semiminor radius of the ellipsoid axis */
79 b("b", true),
80 /** Datum name (see {@code proj -ld}) */
81 datum("datum", true),
82 /** 3 or 7 term datum transform parameters */
83 towgs84("towgs84", true),
84 /** Filename of NTv2 grid file to use for datum transforms */
85 nadgrids("nadgrids", true),
86 /** Projection name (see {@code proj -l}) */
87 proj("proj", true),
88 /** Latitude of origin */
89 lat_0("lat_0", true),
90 /** Latitude of first standard parallel */
91 lat_1("lat_1", true),
92 /** Latitude of second standard parallel */
93 lat_2("lat_2", true),
94 /** the exact proj.4 string will be preserved in the WKT representation */
95 wktext("wktext", false), // ignored
96 /** meters, US survey feet, etc. */
97 units("units", true),
98 /** Don't use the /usr/share/proj/proj_def.dat defaults file */
99 no_defs("no_defs", false),
100 init("init", true),
101 /** crs units to meter multiplier */
102 to_meter("to_meter", true),
103 /** definition of axis for projection */
104 axis("axis", true),
105 /** UTM zone */
106 zone("zone", true),
107 /** indicate southern hemisphere for UTM */
108 south("south", false),
109 // JOSM extensions, not present in PROJ.4
110 wmssrs("wmssrs", true),
111 bounds("bounds", true);
112
113 /** Parameter key */
114 public final String key;
115 /** {@code true} if the parameter has a value */
116 public final boolean hasValue;
117
118 /** Map of all parameters by key */
119 static final Map<String, Param> paramsByKey = new ConcurrentHashMap<>();
120 static {
121 for (Param p : Param.values()) {
122 paramsByKey.put(p.key, p);
123 }
124 }
125
126 Param(String key, boolean hasValue) {
127 this.key = key;
128 this.hasValue = hasValue;
129 }
130 }
131
132 /**
133 * Constructs a new empty {@code CustomProjection}.
134 */
135 public CustomProjection() {
136 // contents can be set later with update()
137 }
138
139 /**
140 * Constructs a new {@code CustomProjection} with given parameters.
141 * @param pref String containing projection parameters
142 * (ex: "+proj=tmerc +lon_0=-3 +k_0=0.9996 +x_0=500000 +ellps=WGS84 +datum=WGS84 +bounds=-8,-5,2,85")
143 */
144 public CustomProjection(String pref) {
145 this(null, null, pref, null);
146 }
147
148 /**
149 * Constructs a new {@code CustomProjection} with given name, code and parameters.
150 *
151 * @param name describe projection in one or two words
152 * @param code unique code for this projection - may be null
153 * @param pref the string that defines the custom projection
154 * @param cacheDir cache directory name
155 */
156 public CustomProjection(String name, String code, String pref, String cacheDir) {
157 this.name = name;
158 this.code = code;
159 this.pref = pref;
160 this.cacheDir = cacheDir;
161 try {
162 update(pref);
163 } catch (ProjectionConfigurationException ex) {
164 try {
165 update(null);
166 } catch (ProjectionConfigurationException ex1) {
167 throw new RuntimeException(ex1);
168 }
169 }
170 }
171
172 /**
173 * Updates this {@code CustomProjection} with given parameters.
174 * @param pref String containing projection parameters (ex: "+proj=lonlat +ellps=WGS84 +datum=WGS84 +bounds=-180,-90,180,90")
175 * @throws ProjectionConfigurationException if {@code pref} cannot be parsed properly
176 */
177 public final void update(String pref) throws ProjectionConfigurationException {
178 this.pref = pref;
179 if (pref == null) {
180 ellps = Ellipsoid.WGS84;
181 datum = WGS84Datum.INSTANCE;
182 proj = new Mercator();
183 bounds = new Bounds(
184 -85.05112877980659, -180.0,
185 85.05112877980659, 180.0, true);
186 } else {
187 Map<String, String> parameters = parseParameterList(pref);
188 ellps = parseEllipsoid(parameters);
189 datum = parseDatum(parameters, ellps);
190 if (ellps == null) {
191 ellps = datum.getEllipsoid();
192 }
193 proj = parseProjection(parameters, ellps);
194 // "utm" is a shortcut for a set of parameters
195 if ("utm".equals(parameters.get(Param.proj.key))) {
196 String zoneStr = parameters.get(Param.zone.key);
197 Integer zone;
198 if (zoneStr == null)
199 throw new ProjectionConfigurationException(tr("UTM projection (''+proj=utm'') requires ''+zone=...'' parameter."));
200 try {
201 zone = Integer.valueOf(zoneStr);
202 } catch (NumberFormatException e) {
203 zone = null;
204 }
205 if (zone == null || zone < 1 || zone > 60)
206 throw new ProjectionConfigurationException(tr("Expected integer value in range 1-60 for ''+zone=...'' parameter."));
207 this.lon0 = 6 * zone - 183;
208 this.k0 = 0.9996;
209 this.x0 = 500000;
210 this.y0 = parameters.containsKey(Param.south.key) ? 10000000 : 0;
211 }
212 String s = parameters.get(Param.x_0.key);
213 if (s != null) {
214 this.x0 = parseDouble(s, Param.x_0.key);
215 }
216 s = parameters.get(Param.y_0.key);
217 if (s != null) {
218 this.y0 = parseDouble(s, Param.y_0.key);
219 }
220 s = parameters.get(Param.lon_0.key);
221 if (s != null) {
222 this.lon0 = parseAngle(s, Param.lon_0.key);
223 }
224 s = parameters.get(Param.k_0.key);
225 if (s != null) {
226 this.k0 = parseDouble(s, Param.k_0.key);
227 }
228 s = parameters.get(Param.bounds.key);
229 if (s != null) {
230 this.bounds = parseBounds(s);
231 }
232 s = parameters.get(Param.wmssrs.key);
233 if (s != null) {
234 this.code = s;
235 }
236 s = parameters.get(Param.units.key);
237 if (s != null) {
238 s = Utils.strip(s, "\"");
239 if (UNITS_TO_METERS.containsKey(s)) {
240 this.metersPerUnit = UNITS_TO_METERS.get(s);
241 } else {
242 Main.warn("No metersPerUnit found for: " + s);
243 }
244 }
245 s = parameters.get(Param.to_meter.key);
246 if (s != null) {
247 this.metersPerUnit = parseDouble(s, Param.to_meter.key);
248 }
249 s = parameters.get(Param.axis.key);
250 if (s != null) {
251 this.axis = s;
252 }
253 }
254 }
255
256 private Map<String, String> parseParameterList(String pref) throws ProjectionConfigurationException {
257 Map<String, String> parameters = new HashMap<>();
258 String[] parts = Utils.WHITE_SPACES_PATTERN.split(pref.trim());
259 if (pref.trim().isEmpty()) {
260 parts = new String[0];
261 }
262 for (String part : parts) {
263 if (part.isEmpty() || part.charAt(0) != '+')
264 throw new ProjectionConfigurationException(tr("Parameter must begin with a ''+'' character (found ''{0}'')", part));
265 Matcher m = Pattern.compile("\\+([a-zA-Z0-9_]+)(=(.*))?").matcher(part);
266 if (m.matches()) {
267 String key = m.group(1);
268 // alias
269 if ("k".equals(key)) {
270 key = Param.k_0.key;
271 }
272 String value = null;
273 if (m.groupCount() >= 3) {
274 value = m.group(3);
275 // some aliases
276 if (key.equals(Param.proj.key)) {
277 if ("longlat".equals(value) || "latlon".equals(value) || "latlong".equals(value)) {
278 value = "lonlat";
279 }
280 }
281 }
282 if (!Param.paramsByKey.containsKey(key))
283 throw new ProjectionConfigurationException(tr("Unknown parameter: ''{0}''.", key));
284 if (Param.paramsByKey.get(key).hasValue && value == null)
285 throw new ProjectionConfigurationException(tr("Value expected for parameter ''{0}''.", key));
286 if (!Param.paramsByKey.get(key).hasValue && value != null)
287 throw new ProjectionConfigurationException(tr("No value expected for parameter ''{0}''.", key));
288 parameters.put(key, value);
289 } else
290 throw new ProjectionConfigurationException(tr("Unexpected parameter format (''{0}'')", part));
291 }
292 // recursive resolution of +init includes
293 String initKey = parameters.get(Param.init.key);
294 if (initKey != null) {
295 String init = Projections.getInit(initKey);
296 if (init == null)
297 throw new ProjectionConfigurationException(tr("Value ''{0}'' for option +init not supported.", initKey));
298 Map<String, String> initp = null;
299 try {
300 initp = parseParameterList(init);
301 } catch (ProjectionConfigurationException ex) {
302 throw new ProjectionConfigurationException(tr(initKey+": "+ex.getMessage()), ex);
303 }
304 for (Map.Entry<String, String> e : parameters.entrySet()) {
305 initp.put(e.getKey(), e.getValue());
306 }
307 return initp;
308 }
309 return parameters;
310 }
311
312 public Ellipsoid parseEllipsoid(Map<String, String> parameters) throws ProjectionConfigurationException {
313 String code = parameters.get(Param.ellps.key);
314 if (code != null) {
315 Ellipsoid ellipsoid = Projections.getEllipsoid(code);
316 if (ellipsoid == null) {
317 throw new ProjectionConfigurationException(tr("Ellipsoid ''{0}'' not supported.", code));
318 } else {
319 return ellipsoid;
320 }
321 }
322 String s = parameters.get(Param.a.key);
323 if (s != null) {
324 double a = parseDouble(s, Param.a.key);
325 if (parameters.get(Param.es.key) != null) {
326 double es = parseDouble(parameters, Param.es.key);
327 return Ellipsoid.create_a_es(a, es);
328 }
329 if (parameters.get(Param.rf.key) != null) {
330 double rf = parseDouble(parameters, Param.rf.key);
331 return Ellipsoid.create_a_rf(a, rf);
332 }
333 if (parameters.get(Param.f.key) != null) {
334 double f = parseDouble(parameters, Param.f.key);
335 return Ellipsoid.create_a_f(a, f);
336 }
337 if (parameters.get(Param.b.key) != null) {
338 double b = parseDouble(parameters, Param.b.key);
339 return Ellipsoid.create_a_b(a, b);
340 }
341 }
342 if (parameters.containsKey(Param.a.key) ||
343 parameters.containsKey(Param.es.key) ||
344 parameters.containsKey(Param.rf.key) ||
345 parameters.containsKey(Param.f.key) ||
346 parameters.containsKey(Param.b.key))
347 throw new ProjectionConfigurationException(tr("Combination of ellipsoid parameters is not supported."));
348 return null;
349 }
350
351 public Datum parseDatum(Map<String, String> parameters, Ellipsoid ellps) throws ProjectionConfigurationException {
352 String datumId = parameters.get(Param.datum.key);
353 if (datumId != null) {
354 Datum datum = Projections.getDatum(datumId);
355 if (datum == null) throw new ProjectionConfigurationException(tr("Unknown datum identifier: ''{0}''", datumId));
356 return datum;
357 }
358 if (ellps == null) {
359 if (parameters.containsKey(Param.no_defs.key))
360 throw new ProjectionConfigurationException(tr("Ellipsoid required (+ellps=* or +a=*, +b=*)"));
361 // nothing specified, use WGS84 as default
362 ellps = Ellipsoid.WGS84;
363 }
364
365 String nadgridsId = parameters.get(Param.nadgrids.key);
366 if (nadgridsId != null) {
367 if (nadgridsId.startsWith("@")) {
368 nadgridsId = nadgridsId.substring(1);
369 }
370 if ("null".equals(nadgridsId))
371 return new NullDatum(null, ellps);
372 NTV2GridShiftFileWrapper nadgrids = Projections.getNTV2Grid(nadgridsId);
373 if (nadgrids == null)
374 throw new ProjectionConfigurationException(tr("Grid shift file ''{0}'' for option +nadgrids not supported.", nadgridsId));
375 return new NTV2Datum(nadgridsId, null, ellps, nadgrids);
376 }
377
378 String towgs84 = parameters.get(Param.towgs84.key);
379 if (towgs84 != null)
380 return parseToWGS84(towgs84, ellps);
381
382 if (parameters.containsKey(Param.no_defs.key))
383 throw new ProjectionConfigurationException(tr("Datum required (+datum=*, +towgs84=* or +nadgrids=*)"));
384 return new CentricDatum(null, null, ellps);
385 }
386
387 public Datum parseToWGS84(String paramList, Ellipsoid ellps) throws ProjectionConfigurationException {
388 String[] numStr = paramList.split(",");
389
390 if (numStr.length != 3 && numStr.length != 7)
391 throw new ProjectionConfigurationException(tr("Unexpected number of arguments for parameter ''towgs84'' (must be 3 or 7)"));
392 List<Double> towgs84Param = new ArrayList<>();
393 for (String str : numStr) {
394 try {
395 towgs84Param.add(Double.valueOf(str));
396 } catch (NumberFormatException e) {
397 throw new ProjectionConfigurationException(tr("Unable to parse value of parameter ''towgs84'' (''{0}'')", str), e);
398 }
399 }
400 boolean isCentric = true;
401 for (Double param : towgs84Param) {
402 if (param != 0) {
403 isCentric = false;
404 break;
405 }
406 }
407 if (isCentric)
408 return new CentricDatum(null, null, ellps);
409 boolean is3Param = true;
410 for (int i = 3; i < towgs84Param.size(); i++) {
411 if (towgs84Param.get(i) != 0) {
412 is3Param = false;
413 break;
414 }
415 }
416 if (is3Param)
417 return new ThreeParameterDatum(null, null, ellps,
418 towgs84Param.get(0),
419 towgs84Param.get(1),
420 towgs84Param.get(2));
421 else
422 return new SevenParameterDatum(null, null, ellps,
423 towgs84Param.get(0),
424 towgs84Param.get(1),
425 towgs84Param.get(2),
426 towgs84Param.get(3),
427 towgs84Param.get(4),
428 towgs84Param.get(5),
429 towgs84Param.get(6));
430 }
431
432 public Proj parseProjection(Map<String, String> parameters, Ellipsoid ellps) throws ProjectionConfigurationException {
433 String id = parameters.get(Param.proj.key);
434 if (id == null) throw new ProjectionConfigurationException(tr("Projection required (+proj=*)"));
435
436 // "utm" is not a real projection, but a shortcut for a set of parameters
437 if ("utm".equals(id)) {
438 id = "tmerc";
439 }
440 Proj proj = Projections.getBaseProjection(id);
441 if (proj == null) throw new ProjectionConfigurationException(tr("Unknown projection identifier: ''{0}''", id));
442
443 ProjParameters projParams = new ProjParameters();
444
445 projParams.ellps = ellps;
446
447 String s;
448 s = parameters.get(Param.lat_0.key);
449 if (s != null) {
450 projParams.lat0 = parseAngle(s, Param.lat_0.key);
451 }
452 s = parameters.get(Param.lat_1.key);
453 if (s != null) {
454 projParams.lat1 = parseAngle(s, Param.lat_1.key);
455 }
456 s = parameters.get(Param.lat_2.key);
457 if (s != null) {
458 projParams.lat2 = parseAngle(s, Param.lat_2.key);
459 }
460 proj.initialize(projParams);
461 return proj;
462 }
463
464 public static Bounds parseBounds(String boundsStr) throws ProjectionConfigurationException {
465 String[] numStr = boundsStr.split(",");
466 if (numStr.length != 4)
467 throw new ProjectionConfigurationException(tr("Unexpected number of arguments for parameter ''+bounds'' (must be 4)"));
468 return new Bounds(parseAngle(numStr[1], "minlat (+bounds)"),
469 parseAngle(numStr[0], "minlon (+bounds)"),
470 parseAngle(numStr[3], "maxlat (+bounds)"),
471 parseAngle(numStr[2], "maxlon (+bounds)"), false);
472 }
473
474 public static double parseDouble(Map<String, String> parameters, String parameterName) throws ProjectionConfigurationException {
475 if (!parameters.containsKey(parameterName))
476 throw new ProjectionConfigurationException(tr("Unknown parameter ''{0}''", parameterName));
477 String doubleStr = parameters.get(parameterName);
478 if (doubleStr == null)
479 throw new ProjectionConfigurationException(
480 tr("Expected number argument for parameter ''{0}''", parameterName));
481 return parseDouble(doubleStr, parameterName);
482 }
483
484 public static double parseDouble(String doubleStr, String parameterName) throws ProjectionConfigurationException {
485 try {
486 return Double.parseDouble(doubleStr);
487 } catch (NumberFormatException e) {
488 throw new ProjectionConfigurationException(
489 tr("Unable to parse value ''{1}'' of parameter ''{0}'' as number.", parameterName, doubleStr), e);
490 }
491 }
492
493 public static double parseAngle(String angleStr, String parameterName) throws ProjectionConfigurationException {
494 String s = angleStr;
495 double value = 0;
496 boolean neg = false;
497 Matcher m = Pattern.compile("^-").matcher(s);
498 if (m.find()) {
499 neg = true;
500 s = s.substring(m.end());
501 }
502 final String FLOAT = "(\\d+(\\.\\d*)?)";
503 boolean dms = false;
504 double deg = 0.0, min = 0.0, sec = 0.0;
505 // degrees
506 m = Pattern.compile("^"+FLOAT+"d").matcher(s);
507 if (m.find()) {
508 s = s.substring(m.end());
509 deg = Double.parseDouble(m.group(1));
510 dms = true;
511 }
512 // minutes
513 m = Pattern.compile("^"+FLOAT+"'").matcher(s);
514 if (m.find()) {
515 s = s.substring(m.end());
516 min = Double.parseDouble(m.group(1));
517 dms = true;
518 }
519 // seconds
520 m = Pattern.compile("^"+FLOAT+"\"").matcher(s);
521 if (m.find()) {
522 s = s.substring(m.end());
523 sec = Double.parseDouble(m.group(1));
524 dms = true;
525 }
526 // plain number (in degrees)
527 if (dms) {
528 value = deg + (min/60.0) + (sec/3600.0);
529 } else {
530 m = Pattern.compile("^"+FLOAT).matcher(s);
531 if (m.find()) {
532 s = s.substring(m.end());
533 value += Double.parseDouble(m.group(1));
534 }
535 }
536 m = Pattern.compile("^(N|E)", Pattern.CASE_INSENSITIVE).matcher(s);
537 if (m.find()) {
538 s = s.substring(m.end());
539 } else {
540 m = Pattern.compile("^(S|W)", Pattern.CASE_INSENSITIVE).matcher(s);
541 if (m.find()) {
542 s = s.substring(m.end());
543 neg = !neg;
544 }
545 }
546 if (neg) {
547 value = -value;
548 }
549 if (!s.isEmpty()) {
550 throw new ProjectionConfigurationException(
551 tr("Unable to parse value ''{1}'' of parameter ''{0}'' as coordinate value.", parameterName, angleStr));
552 }
553 return value;
554 }
555
556 @Override
557 public Integer getEpsgCode() {
558 if (code != null && code.startsWith("EPSG:")) {
559 try {
560 return Integer.valueOf(code.substring(5));
561 } catch (NumberFormatException e) {
562 Main.warn(e);
563 }
564 }
565 return null;
566 }
567
568 @Override
569 public String toCode() {
570 return code != null ? code : "proj:" + (pref == null ? "ERROR" : pref);
571 }
572
573 @Override
574 public String getCacheDirectoryName() {
575 return cacheDir != null ? cacheDir : "proj-"+Utils.md5Hex(pref == null ? "" : pref).substring(0, 4);
576 }
577
578 @Override
579 public Bounds getWorldBoundsLatLon() {
580 if (bounds != null) return bounds;
581 return new Bounds(
582 new LatLon(-90.0, -180.0),
583 new LatLon(90.0, 180.0));
584 }
585
586 @Override
587 public String toString() {
588 return name != null ? name : tr("Custom Projection");
589 }
590
591 @Override
592 public double getMetersPerUnit() {
593 return metersPerUnit;
594 }
595
596 @Override
597 public boolean switchXY() {
598 // TODO: support for other axis orientation such as West South, and Up Down
599 return this.axis.startsWith("ne");
600 }
601
602 private static Map<String, Double> getUnitsToMeters() {
603 Map<String, Double> ret = new ConcurrentHashMap<>();
604 ret.put("km", 1000d);
605 ret.put("m", 1d);
606 ret.put("dm", 1d/10);
607 ret.put("cm", 1d/100);
608 ret.put("mm", 1d/1000);
609 ret.put("kmi", 1852.0);
610 ret.put("in", 0.0254);
611 ret.put("ft", 0.3048);
612 ret.put("yd", 0.9144);
613 ret.put("mi", 1609.344);
614 ret.put("fathom", 1.8288);
615 ret.put("chain", 20.1168);
616 ret.put("link", 0.201168);
617 ret.put("us-in", 1d/39.37);
618 ret.put("us-ft", 0.304800609601219);
619 ret.put("us-yd", 0.914401828803658);
620 ret.put("us-ch", 20.11684023368047);
621 ret.put("us-mi", 1609.347218694437);
622 ret.put("ind-yd", 0.91439523);
623 ret.put("ind-ft", 0.30479841);
624 ret.put("ind-ch", 20.11669506);
625 ret.put("degree", METER_PER_UNIT_DEGREE);
626 return ret;
627 }
628}
Note: See TracBrowser for help on using the repository browser.