/** * * Copyright 2012-2013 The MITRE Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * ************************************************************************** * NOTICE * This software was produced for the U. S. Government under Contract No. * W15P7T-12-C-F600, and is subject to the Rights in Noncommercial Computer * Software and Noncommercial Computer Software Documentation Clause * 252.227-7014 (JUN 1995) * * (c) 2012 The MITRE Corporation. All Rights Reserved. * ************************************************************************** */ package org.opensextant.extractors.xcoord; /* # # http://en.wikipedia.org/wiki/Wikipedia:WikiProject_Geographical_coordinates#Precision */ import java.text.DecimalFormat; import org.opensextant.util.TextUtils; /** * @author ubaldino * */ public class PrecisionScales { /** * */ public static final float DEFAULT_UNKNOWN_RESOLUTION = 111000f; // Lat 1deg = 111KM approx. maximum /** * */ public static final float LAT_DEGREE_PRECISION = (int) (DEFAULT_UNKNOWN_RESOLUTION / 2); /** * Maximal error in METERS in coordinate with N decimal places; for up to 12 * decimal places. */ public static final double[] DD_precision_list = { // LAT_DEGREE_PRECISION, // 0 // LAT_DEGREE_PRECISION / 10, // 1 // LAT_DEGREE_PRECISION / 100, // 2, ... etc. // LAT_DEGREE_PRECISION / 1000, // LAT_DEGREE_PRECISION / 10000, // Approx 1m precision at equator: LAT_DEGREE_PRECISION / 100000, // LAT_DEGREE_PRECISION / 1000000, // LAT_DEGREE_PRECISION / 10000000, // LAT_DEGREE_PRECISION / 100000000, // LAT_DEGREE_PRECISION / 1000000000, // LAT_DEGREE_PRECISION / 10000000000L, // LAT_DEGREE_PRECISION / 100000000000L, // LAT_DEGREE_PRECISION / 1000000000000L }; // Last entry above. static final double FINEST_DD_PRECISION = LAT_DEGREE_PRECISION / 1000000000000L; /** * Sets the precision on a decimal degrees match * * @param m given match */ public static void setDDPrecision(GeocoordMatch m) { m.precision.setDigits(count_DD_digits(m.lat_text)); m.precision.setDigits(count_DD_digits(m.lon_text)); if (m.precision.digits < DD_precision_list.length) { m.precision.precision = DD_precision_list[m.precision.digits]; } else if (m.precision.digits > DD_precision_list.length) { m.precision.precision = FINEST_DD_PRECISION; } else { m.precision.precision = DEFAULT_UNKNOWN_RESOLUTION; } } /** * Return XCoord precision (+/- meters) in latitude. * @param lat string representing latitude * @return precision */ public static GeocoordPrecision getDDPrecision(String lat) { GeocoordPrecision prec = new GeocoordPrecision(); // Find the number of digits used in lat. prec.setDigits(count_DD_digits(lat)); // Determine the error given the number of digits if (prec.digits < DD_precision_list.length) { prec.precision = DD_precision_list[prec.digits]; } else if (prec.digits > DD_precision_list.length) { prec.precision = FINEST_DD_PRECISION; } else { prec.precision = DEFAULT_UNKNOWN_RESOLUTION; } return prec; } /** * Count the number of decimal places in a lat or lon text string. * @param lat string representing latitude * @return number of digits in lat, as a proxy for precision */ public static int count_DD_digits(String lat) { if (lat == null) { return 0; } int x = lat.indexOf('.'); // "decimal point" Cannot be offset zero if (x <= 0) { return 0; } String decPart = lat.substring(x); return TextUtils.count_digits(decPart); } /** * Counts all digits in latitude. * * @param lat string representing latitude * @return number of digits in lat as a proxy for precision */ public static int count_DMS_digits(String lat) { return TextUtils.count_digits(lat); } /** * */ public static float DMS_MIN_PREC = 900; // +/- 1.85 KM at equator /** * */ public static float DMS_SEC_PREC = 15; // +/- 0.03 KM at equator, 60th of a minute /** * set precision on a DMS text coordinate * * @param m DMS match */ public static void setDMSPrecision(GeocoordMatch m) { m.precision.precision = LAT_DEGREE_PRECISION; if (m.lat_text != null) { int dig = count_DMS_digits(m.lat_text); int deg = Math.abs((int) m.getLatitude()); if (deg < 10) { --dig; } else { // 10 <= Lat <= 90 ---> Means it is a 2digit string. --dig; --dig; } m.precision.digits = dig; if (dig >= 4) { m.precision.precision = DMS_SEC_PREC; } else if (dig >= 2) { m.precision.precision = DMS_MIN_PREC; } return; } if (m.hasSeconds()) { m.precision.precision = DMS_SEC_PREC; m.precision.digits = 5; // 1/3600th of a degree } else if (m.hasMinutes()) { m.precision.precision = DMS_MIN_PREC; m.precision.digits = 2; // 1/60th of a degree } } /** * * @param m MGRS match */ public static void setMGRSPrecision(GeocoordMatch m) { if (m.coord_text == null) { return; } /* * Given MGRS padded, but no whitespace -- the length indicates precision. */ int len = m.coord_text.length(); if (len < 17) { m.precision.precision = MGRS_precision_list[len]; m.precision.digits = MGRS2DEC_digits[len]; } } /** * * @param m UTM match */ public static void setUTMPrecision(GeocoordMatch m) { m.precision.precision = UTM_precision(m.getText()); m.precision.digits = 5; // seconds resolution } /** * Precision appears in pairs, as we tolerate some typo/errors in MGRs. * */ public static float[] MGRS_precision_list = { 100000, // 1 GZD 100000, // 2 GZD 100000, // 3 GZD 100000, // 4 Q 100000, // 5 Q 100000, // 6, same as 5; odd # of chars usually means typo. This would be first char in E/N'ing // --------------------------- 10000, // DEC PREC: 1 digit 10000, // 1000, // 3 digits 1000, // MINUTES PREC 3 digits 100, // 4 digits 100, // 4 10, // SECONDS PREC 6 digits 10, 1, 1, 0.1f, // 16 0.1f // 17 }; /** * */ public static int[] MGRS2DEC_digits = { 0, // 1 GZD 0, // 2 GZD 0, // 3 GZD 0, // 4 Q 0, // 5 Q 0, // 6, same as 5; odd # of chars usually means typo. This would be first char in E/N'ing // --------------------------- 1, // DEC PREC: 1 digit 1, // 3, // 3 digits 3, // MINUTES PREC 3 digits 4, // 4 digits 4, // 4 6, // SECONDS PREC 6 digits 6, 7, 7, 8, // 16 8 // 17 }; /** * For now default UTM precision to +/- 100m * @param utm UTM string * @return precision */ public static float UTM_precision(String utm) { return 100f; } /** * Numeric formatters are cached here for performance. number of digits of * precision depends on lat (%2.12f) vs. lon (%3.12f) * WARNING: these are NOT thread-safe! */ private static final DecimalFormat[] formatters; /* Static initialization here creates and caches all the numeric formatters * TODO: however at runtime the formatters are cloned (as of v1.6a) so is there any benefit? */ static { formatters = new DecimalFormat[12]; formatters[0] = new DecimalFormat("0"); StringBuilder buf = new StringBuilder("0."); for (int i = 1; i < formatters.length; i++) { buf.append('0'); formatters[i] = new DecimalFormat(buf.toString()); } } /** * This was deemed to be more Java like, however performs 10x slower than * format2() -- which unfortunately rounds too early. * @param f value * @param digits digits to include in format * @return formatted value. */ public static String format(double f, int digits) { if (digits >= formatters.length) { return "" + f; } //Clone the formatter so that it's thread-safe // TODO: well, we would do this clone for every time this method is called. // to make such things MT-safe and prevent too much impact on performance, we should do a // more thorough implementation review of such static variables. DecimalFormat df = (DecimalFormat) formatters[digits].clone(); return df.format(f); } }