/** Copyright (C) SYSTAP, LLC DBA Blazegraph 2006-2016. All rights reserved. Contact: SYSTAP, LLC DBA Blazegraph 2501 Calvert ST NW #106 Washington, DC 20008 licenses@blazegraph.com This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 2 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ /* * Created on Aug 6, 2007 */ package com.bigdata.rdf.internal.gis; import java.text.ParseException; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * An immutable coordinate expressed as degrees and decimal minutes with 3 * digits after the decimal. * * <pre> * Degrees and Decimal Minutes * * DDD� MM.MMM' * 32� 18.385' N 122� 36.875' W * </pre> * * @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson</a> * @version $Id$ */ public class CoordinateDDM implements ICoordinate { public static Logger log = Logger.getLogger(CoordinateDDM.class.getName()); public final int degreesNorth; public final int thousandthsOfMinutesNorth; public final int degreesEast; public final int thousandthsOfMinutesEast; /** * * @param degreesNorth * Degrees north/south. * @param thousandthsOfMinutesNorth * Decimal minutes north/south expressed as minutes * 1000. * @param degreesEast * Degrees east/west. * @param thousandthsOfMinutesEast * Decimal minutes east/west expressed as minutes * 1000. */ public CoordinateDDM(// int degreesNorth, int thousandthsOfMinutesNorth,// int degreesEast, int thousandthsOfMinutesEast // ) { if (degreesNorth > 90 || degreesNorth < -90) throw new IllegalArgumentException(); if (thousandthsOfMinutesNorth > 60000 || thousandthsOfMinutesNorth < -60000) throw new IllegalArgumentException(); if (degreesEast > 180 || degreesEast < -180) throw new IllegalArgumentException(); if (thousandthsOfMinutesEast > 60000 || thousandthsOfMinutesEast < -60000) throw new IllegalArgumentException(); /* * If the angle is negative, then all components must be negative. */ if (degreesNorth < 0) { if (thousandthsOfMinutesNorth > 0) throw new IllegalArgumentException(); } if (degreesEast < 0) { if (thousandthsOfMinutesEast > 0) throw new IllegalArgumentException(); } /* * @todo normalize -180 to 180; do we also have to fiddle with the * minutes and seconds? */ this.degreesNorth = degreesNorth; this.thousandthsOfMinutesNorth = thousandthsOfMinutesNorth; this.degreesEast = degreesEast; this.thousandthsOfMinutesEast = thousandthsOfMinutesEast; } /** * Representation of the coordinate in degrees and (thousandths of) decimal * minutes. For example: * * <pre> * 32 18.385N 122 36.875W * </pre> */ public String toString() { boolean northSouth = degreesNorth > 0; boolean eastWest = degreesEast > 0; String north = "" + (northSouth ? degreesNorth : -degreesNorth) + " " + formatThousandthsOfMinute(northSouth ? thousandthsOfMinutesNorth : -thousandthsOfMinutesNorth) + (northSouth ? "N" : "S"); String east = (eastWest ? "" + degreesEast : -degreesEast) + " " + formatThousandthsOfMinute(eastWest ? thousandthsOfMinutesEast : -thousandthsOfMinutesEast) + (eastWest ? "E" : "W"); return north + " " + east; } /** * Formats a value expressing tenths of a second. For example, * <code>18385</code> is formatted as <code>18.385</code>. * <p> * Note: This does NOT correct for South or West (negative angles). * * @param thousandthsOfMinute * An integer value containing minutes and thousandths of a * minute. * * @return A string representation of that value. */ static String formatThousandthsOfMinute(int thousandthsOfMinute) { int minutes = thousandthsOfMinute / 1000; int thousandths = thousandthsOfMinute - minutes * 1000; return minutes + "." + thousandths; } /** * Matches a latitude expressed with any of the formats for expressing * degrees and decimal minutes. * <dl> * <dt>group(2)</dt> * <dd>degrees</dd> * <dt>group(3)</dt> * <dd>decimal minutes</dd> * <dt>group(4)</dt> * <dd>north/south (NnSs)</dd> * </dl> */ static final String regex_lat = // "(" + "(\\d{1,2})\\s?[:�*\\s]?\\s?" + // degrees (0:90) "(\\d{1,2}|\\d{1,2}\\.\\d*)\\s?[:'\\s]?\\s?" + // decimal minutes "([NnSs\\s])" + // north/south ")"; /** * Matches a longitude expressed with any of the formats for expressing * degrees and decimal minutes. * * <dl> * <dt>group(2)</dt> * <dd>degrees</dd> * <dt>group(3)</dt> * <dd>decimal minutes</dd> * <dt>group(4)</dt> * <dd>east/west (EeWw)</dd> * </dl> */ static final String regex_long = // "(" + "(\\d{1,3})\\s?[:�*\\s]?\\s?" + // degrees (0:180) "(\\d{1,2}|\\d{1,2}\\.\\d*)\\s?[:'\\s]?\\s?" + // decimal minutes "([EeWw])" + // east/west ")"; /** * Matches any of the formats for degrees and decimal minutes. * <dl> * <dt>group({@link #group_degreesNorth})</dt> * <dd>degrees</dd> * <dt>group({@link #group_minutesNorth})</dt> * <dd>minutes</dd> * <dt>group({@link #group_northSouth})</dt> * <dd>north/south (NnSs)</dd> * <dt>group({@link #group_degreesEast})</dt> * <dd>degrees</dd> * <dt>group({@link #group_minutesEast})</dt> * <dd>minutes</dd> * <dt>group({@link #group_eastWest})</dt> * <dd>east/west (EeWw)</dd> * </dl> * * @see #regex_lat * @see #regex_long */ static final Pattern pattern_ddm = Pattern.compile("^(" + // "(" + regex_lat + "(\\s?[/,]?\\s?)" + regex_long + ")" + // ")$"// ); /* * The integer identifier of the group in pattern_ddm in which the * corresponding named component of the coordinate will be found. */ static final int group_degreesNorth = 4; static final int group_minutesNorth = 5; static final int group_northSouth = 6; static final int group_degreesEast = 9; static final int group_minutesEast = 10; static final int group_eastWest = 11; /** * Some formats that are accepted: * * <ul> * * <li>32� 18.385' N 122� 36.875' W</li> * * <li>32 18.385N 122 36.875W</li> * * <li>32 18.385 N 122 36.875 W</li> * * <li>32:18.385N 122:36.875W</li> * * <li>32:18:23.1N/122:36:52.5W</li> * * </ul> */ public static CoordinateDDM parse(String text) throws ParseException { // See the pattern for the matched groups. Matcher m = pattern_ddm.matcher(text); if (m.matches()) { final int degreesNorth, thousandthsOfMinutesNorth; final boolean northSouth; try { degreesNorth = Integer.parseInt(m.group(group_degreesNorth)); } catch (NumberFormatException ex) { log.log(Level.WARNING, "Parsing text: [" + text + "]", ex); throw ex; } // @todo round vs truncate. thousandthsOfMinutesNorth = (int) (Float.parseFloat(m .group(group_minutesNorth)) * 1000); // Note: Use of double negative gets the default right (N). northSouth = !"S".equalsIgnoreCase(m.group(group_northSouth)); // northSouth = "N".equalsIgnoreCase(m.group(group_northSouth)); final int degreesEast, thousandthsOfMinutesEast; final boolean eastWest; degreesEast = Integer.parseInt(m.group(group_degreesEast)); // @todo round vs truncate. thousandthsOfMinutesEast = (int) (Float.parseFloat(m .group(group_minutesEast)) * 1000); // Note: Use of double negative gets the default right (E). eastWest = !"W".equalsIgnoreCase(m.group(group_eastWest)); // eastWest = "E".equalsIgnoreCase(m.group(group_eastWest)); /* * Note: When South or West then all components of the angle are * negative. */ return new CoordinateDDM( // northSouth ? degreesNorth : -degreesNorth,// northSouth ? thousandthsOfMinutesNorth : -thousandthsOfMinutesNorth,// eastWest ? degreesEast : -degreesEast,// eastWest ? thousandthsOfMinutesEast : -thousandthsOfMinutesEast// ); } throw new ParseException("Not recognized: " + text, 0); } public boolean equals(ICoordinate o) { if (o instanceof CoordinateDDM) { return equals((CoordinateDDM) o); } return false; } /** * Equal if the coordinates are exactly the same (precision to thousandths * of a minute). * * @param o * Another coordinate expressed in decimal minutes. */ public boolean equals(CoordinateDDM o) { return degreesNorth == o.degreesNorth && thousandthsOfMinutesNorth == o.thousandthsOfMinutesNorth && degreesEast == o.degreesEast && thousandthsOfMinutesEast == o.thousandthsOfMinutesEast; } public double distance(ICoordinate o, UNITS units) { return CoordinateUtility.distance(toDD(), o.toDD(), units); } /** * Convert to degrees, minutes and (tenths of) seconds. */ public CoordinateDMS toDMS() { // FIXME implement toDMS() throw new UnsupportedOperationException(); } /** * Convert to decimal degrees. */ public CoordinateDD toDD() { return new CoordinateDD( // degreesNorth + (thousandthsOfMinutesNorth / 1000d) / 60d, degreesEast + (thousandthsOfMinutesEast / 1000d) / 60d); } public CoordinateDDM toDDM() { return this; } }