//Dstl (c) Crown Copyright 2017 package uk.gov.dstl.common.geo.osgb; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import com.google.common.base.CharMatcher; import com.google.common.base.Splitter; import com.google.common.primitives.Doubles; /** Convert National grid (TM 123412 23434) to OSGB Northing and Easting. * */ public class NationalGrid { private static final Splitter WHITESPACE_SPLITTER = Splitter.on(CharMatcher.whitespace()).omitEmptyStrings().trimResults(); private static final double SIZE_M = 100000; private static final class GridSquare { private final String reference; private final double northing; private final double easting; /** * Constructor to create a new GridSquare for the given reference, easting and northing * * @param reference * @param easting * @param northing */ public GridSquare(String reference, double easting, double northing) { this.reference = reference; this.northing = northing; this.easting = easting; } /** * Return the Reference of this grid square (e.g. SU) */ public String getReference() { return reference; } /** * Is the given Easting-Northing pair inside this grid square? * * @param en An array of Easting-Northing coordinates (in that order) */ public final boolean inside(double[] en) { return inside(en[0], en[1]); } /** * Is the given Easting-Northing pair inside this grid square? * * @param e Easting * @param n Northing */ public final boolean inside(double e, double n) { return northing <= n && n < northing + SIZE_M && easting <= e && e < easting + SIZE_M; } /** * Return the offset of the provided absolute Easting-Northing within this grid square * * @param en An array of Easting-Northing coordinates (in that order) */ public double[] offsetEastingNorthing(double[] en) { return offsetEastingNorthing(en[0], en[1]); } /** * Return the offset of the provided absolute Easting-Northing within this grid square * * @param e Easting * @param n Northing */ public double[] offsetEastingNorthing(double e, double n) { return new double[] { e - easting, n - northing }; } /** * Convert an Easting-Northing from a relative (to this grid square) pair to an absolute pair * * @param e Easting * @param n Northing */ public double[] toEastingNorthing(double e, double n) { return new double[] { e + easting, n + northing }; } } private static final Map<String, GridSquare> GRID_SQUARES; static { GRID_SQUARES = new HashMap<String, NationalGrid.GridSquare>(); addGridSquare("SV", 0, 0); addGridSquare("SW", 1, 0); addGridSquare("SX", 2, 0); addGridSquare("SY", 3, 0); addGridSquare("SZ", 4, 0); addGridSquare("TV", 5, 0); addGridSquare("TW", 6, 0); addGridSquare("SQ", 0, 1); addGridSquare("SR", 1, 1); addGridSquare("SS", 2, 1); addGridSquare("ST", 3, 1); addGridSquare("SU", 4, 1); addGridSquare("TQ", 5, 1); addGridSquare("TR", 6, 1); addGridSquare("SL", 0, 2); addGridSquare("SM", 1, 2); addGridSquare("SN", 2, 2); addGridSquare("SO", 3, 2); addGridSquare("SP", 4, 2); addGridSquare("TL", 5, 2); addGridSquare("TM", 6, 2); addGridSquare("SF", 0, 3); addGridSquare("SG", 1, 3); addGridSquare("SH", 2, 3); addGridSquare("SJ", 3, 3); addGridSquare("SK", 4, 3); addGridSquare("TF", 5, 3); addGridSquare("TG", 6, 3); addGridSquare("SA", 0, 4); addGridSquare("SB", 1, 4); addGridSquare("SC", 2, 4); addGridSquare("SD", 3, 4); addGridSquare("SE", 4, 4); addGridSquare("TA", 5, 4); addGridSquare("TB", 6, 4); // n AND o addGridSquare("NV", 0, 5); addGridSquare("NW", 1, 5); addGridSquare("NX", 2, 5); addGridSquare("NY", 3, 5); addGridSquare("NZ", 4, 5); addGridSquare("OV", 5, 5); addGridSquare("OW", 6, 5); addGridSquare("NQ", 0, 6); addGridSquare("NR", 1, 6); addGridSquare("NS", 2, 6); addGridSquare("NT", 3, 6); addGridSquare("NU", 4, 6); addGridSquare("OQ", 5, 6); addGridSquare("OR", 6, 6); addGridSquare("NL", 0, 7); addGridSquare("NM", 1, 7); addGridSquare("NN", 2, 7); addGridSquare("NO", 3, 7); addGridSquare("NP", 4, 7); addGridSquare("OL", 5, 7); addGridSquare("OM", 6, 7); addGridSquare("NF", 0, 8); addGridSquare("NG", 1, 8); addGridSquare("NH", 2, 8); addGridSquare("NJ", 3, 8); addGridSquare("NK", 4, 8); addGridSquare("OF", 5, 8); addGridSquare("OG", 6, 8); addGridSquare("NA", 0, 9); addGridSquare("NB", 1, 9); addGridSquare("NC", 2, 9); addGridSquare("ND", 3, 9); addGridSquare("NE", 4, 9); addGridSquare("OA", 5, 9); addGridSquare("OB", 6, 9); // h & j addGridSquare("HV", 0, 10); addGridSquare("HW", 1, 10); addGridSquare("HX", 2, 10); addGridSquare("HY", 3, 10); addGridSquare("HZ", 4, 10); addGridSquare("JV", 5, 10); addGridSquare("JW", 6, 10); addGridSquare("HQ", 0, 11); addGridSquare("HR", 1, 11); addGridSquare("HS", 2, 11); addGridSquare("HT", 3, 11); addGridSquare("HU", 4, 11); addGridSquare("JQ", 5, 11); addGridSquare("JR", 6, 11); addGridSquare("HL", 0, 12); addGridSquare("HM", 1, 12); addGridSquare("HN", 2, 12); addGridSquare("HO", 3, 12); addGridSquare("HP", 4, 12); addGridSquare("JL", 5, 12); addGridSquare("JM", 6, 12); } private NationalGrid() { //Utility class - private constructor } private static void addGridSquare(String reference, int x, int y) { GRID_SQUARES.put(reference, new GridSquare(reference, x*SIZE_M, y*SIZE_M)); } /** Convert from a string to northing and easting * @param ng the national grid string * @return array [n,e] */ public static double[] fromNationalGrid(String ng) { String trimmed = ng.trim(); String ref = trimmed.substring(0,2); GridSquare gridSquare = GRID_SQUARES.get(ref); if(gridSquare == null) { throw new IllegalArgumentException("Invalid NG: "+trimmed); } List<String> list = WHITESPACE_SPLITTER.splitToList(trimmed.substring(2)); Double n = null; Double e = null; if(list.size() >= 2) { // Have two values so use them e = parseDoubleWithCoordPrecision(list.get(0)); n = parseDoubleWithCoordPrecision(list.get(1)); } else if(list.size() == 1) { // Consolidated value String[] ret = splitConsolidated(list.get(0)); e = parseDoubleWithCoordPrecision(ret[0]); n = parseDoubleWithCoordPrecision(ret[1]); } else { throw new IllegalArgumentException("Invalid NG coords "+ng); } if(n == null || e == null) { throw new IllegalArgumentException("Unable to extract NE from "+ng); } return gridSquare.toEastingNorthing(e, n); } /** * Split a string into two Strings * * @param s The string to split * @return An array, containing the Easting and Northing (in that order) split from a string */ private static String[] splitConsolidated(String s){ if((s.length() % 2) != 0) { throw new IllegalArgumentException("Differing size of northing and easting, unable to determine valid ref "+s); } int index = s.length() / 2; return new String[]{ s.substring(0, index), //Easting s.substring(index) //Northing }; } private static Double parseDoubleWithCoordPrecision(String s){ int precedingZeroes = 0; String t = s; while(t.startsWith("0")){ precedingZeroes++; t = t.substring(1); } Double c = Doubles.tryParse(t); if(c == null){ return null; } double multiplier = Math.pow(10, 4 - precedingZeroes - Math.floor(Math.log10(c))); return c * multiplier; } /** Convert EastingsNorthings to a NationalGrid reference. * * This is not a very optimised implementation. * * @param en array of {e,n} * @return optional empty if the ne has no representation */ public static Optional<String> toNationalGrid(double[] en) { for(GridSquare gs : GRID_SQUARES.values()) { if(gs.inside(en)) { double[] offset = gs.offsetEastingNorthing(en); return Optional.of( String.format("%s %05.0f %05.0f", gs.getReference(), offset[0], offset[1]) ); } } return Optional.empty(); } }