/** 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 */ 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 in degrees, minutes and (tenths of) * seconds. * <p> * Note: When the angle is negative, all components must be negative. For * example, 0794731W corresponds to -79 deg 47' 31.111439999999998", but the * individual components are actually -79, -47, and -31.1... * * @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson</a> * @version $Id$ */ public class CoordinateDMS implements ICoordinate { public static Logger log = Logger.getLogger(CoordinateDMS.class.getName()); public final int degreesNorth; public final int minutesNorth; public final int tenthsOfSecondsNorth; public final int degreesEast; public final int minutesEast; public final int tenthsOfSecondsEast; public CoordinateDMS(// int degreesNorth, int minutesNorth, int tenthsOfSecondsNorth,// int degreesEast, int minutesEast, int tenthsOfSecondsEast// ) { if (degreesNorth > 90 || degreesNorth < -90) throw new IllegalArgumentException(); if (minutesNorth > 60 || minutesNorth < -60) throw new IllegalArgumentException(); if (tenthsOfSecondsNorth > 600 || tenthsOfSecondsNorth < -600) throw new IllegalArgumentException(); if (degreesEast > 180 || degreesEast < -180) throw new IllegalArgumentException(); if (minutesEast > 60 || minutesEast < -60) throw new IllegalArgumentException(); if (tenthsOfSecondsEast > 600 || tenthsOfSecondsEast < -600) throw new IllegalArgumentException(); /* * If the angle is negative, then all components must be negative. */ if (degreesNorth < 0) { if (minutesNorth > 0) throw new IllegalArgumentException(); if (tenthsOfSecondsNorth > 0) throw new IllegalArgumentException(); } if (degreesEast < 0) { if (minutesEast > 0) throw new IllegalArgumentException(); if (tenthsOfSecondsEast > 0) throw new IllegalArgumentException(); } /* * @todo normalize -180 to 180; do we also have to fiddle with the * minutes and seconds? */ this.degreesNorth = degreesNorth; this.minutesNorth = minutesNorth; this.tenthsOfSecondsNorth = tenthsOfSecondsNorth; this.degreesEast = degreesEast; this.minutesEast = minutesEast; this.tenthsOfSecondsEast = tenthsOfSecondsEast; } /** * Representation of the coordinate in degrees, minutes, and (tenths of) * seconds. For example: * * <pre> * 32 18 23.1N 122 36 52.5W * </pre> */ public String toString() { boolean northSouth = degreesNorth > 0; boolean eastWest = degreesEast > 0; String north = "" + (northSouth ? degreesNorth : -degreesNorth) + " " + (northSouth ? minutesNorth : -minutesNorth) + " " + formatTenthsOfSecond(northSouth ? tenthsOfSecondsNorth : -tenthsOfSecondsNorth) + (northSouth ? "N" : "S"); String east = (eastWest ? "" + degreesEast : -degreesEast) + " " + (eastWest ? minutesEast : -minutesEast) + " " + formatTenthsOfSecond(eastWest ? tenthsOfSecondsEast : -tenthsOfSecondsEast) + (eastWest ? "E" : "W"); return north + " " + east; } /** * Rounds off the coordinate to the nearest seconds. * * @return A new coordinate that has been rounded off to the nearest * seconds. */ public CoordinateDMS roundSeconds() { int secondsNorth = (tenthsOfSecondsNorth > 0 ? (int) Math .round(tenthsOfSecondsNorth / 10.) : -(int) Math .round(-tenthsOfSecondsNorth / 10.)); int secondsEast = (tenthsOfSecondsEast > 0 ? (int) Math .round(tenthsOfSecondsEast / 10.) : -(int) Math .round(-tenthsOfSecondsEast / 10.)); return new CoordinateDMS(// degreesNorth, minutesNorth, secondsNorth * 10,// degreesEast, minutesEast, secondsEast * 10// ); } /** * Rounds off the coordinate to the nearest minutes (rounds up at 30.0 * seconds to the next highest minute). * * @return A new coordinate that has been rounded off to the nearest * minutes. */ public CoordinateDMS roundMinutes() { final int roundMinutesNorth = (tenthsOfSecondsNorth > 300 ? minutesNorth + 1 : (tenthsOfSecondsNorth < -300 ? minutesNorth - 1 : minutesNorth)); final int roundMinutesEast = (tenthsOfSecondsEast > 300 ? minutesEast + 1 : (tenthsOfSecondsEast < -300 ? minutesEast - 1 : minutesEast)); return new CoordinateDMS( // degreesNorth, roundMinutesNorth, 0, degreesEast, roundMinutesEast, 0// ); } public boolean equals(ICoordinate o) { if (o instanceof CoordinateDMS) { return equals((CoordinateDMS) o); } return false; } /** * True iff the two coordinates are exactly the same (to the tenths of the * second). * * @param o * Another coordinate. * * @return True if the coordinates are exactly the same. */ public boolean equals(CoordinateDMS o) { return degreesNorth == o.degreesNorth && minutesNorth == o.minutesNorth && tenthsOfSecondsNorth == o.tenthsOfSecondsNorth && degreesEast == o.degreesEast && minutesEast == o.minutesEast && tenthsOfSecondsEast == o.tenthsOfSecondsEast; } /** * Some formats that are accepted: * * <ul> * * <li>32� 18' 23.1" N 122� 36' 52.5" W</li> * * <li>32 18 23.1N 122 36 52.5 W</li> * * <li>32 18 23.1N/122 36 52.5W</li> * * <li>32:18:23N/122:36:52W</li> * * <li>32:18:23.1N/122:36:52.5W</li> * * <li>321823N/1223652W <br/> (zeros would need to go in front of single * digits and two zeros in front of the longitude degrees because it�s range * is up to 180 � the latitude range is only up to 90)</li> * * <li>3218N/12236W</li> * * </ul> */ public static CoordinateDMS parse(String text) throws ParseException { // See the pattern for the matched groups. Matcher m = pattern_dms1.matcher(text); if (m.matches()) { final int degreesNorth, minutesNorth, tenthsOfSecondsNorth; final boolean northSouth; try { degreesNorth = Integer.parseInt(m.group(group_degreesNorth)); } catch (NumberFormatException ex) { log.log(Level.WARNING, "Parsing text: [" + text + "]", ex); throw ex; } minutesNorth = Integer.parseInt(m.group(group_minutesNorth)); // Note: null iff group not matched. "" if matched on a zero length // production. if (m.group(group_secondsNorth) != null && m.group(group_secondsNorth).length() > 0) { // @todo round vs truncate. tenthsOfSecondsNorth = (int) (Float.parseFloat(m .group(group_secondsNorth)) * 10); } else { tenthsOfSecondsNorth = 0; } // Note: Use of double negative gets the default right (N). northSouth = !"S".equalsIgnoreCase(m.group(group_northSouth)); final int degreesEast, minutesEast, tenthsOfSecondsEast; final boolean eastWest; degreesEast = Integer.parseInt(m.group(group_degreesEast)); minutesEast = Integer.parseInt(m.group(group_minutesEast)); if (m.group(group_secondsEast) != null && m.group(group_secondsEast).length() > 0) { // @todo round vs truncate. tenthsOfSecondsEast = (int) (Float.parseFloat(m .group(group_secondsEast)) * 10); } else { tenthsOfSecondsEast = 0; } // Note: Use of double negative gets the default right (E). eastWest = !"W".equalsIgnoreCase(m.group(group_eastWest)); /* * Note: When South or West then all components of the angle are * negative. */ return new CoordinateDMS( // northSouth ? degreesNorth : -degreesNorth,// northSouth ? minutesNorth : -minutesNorth,// northSouth ? tenthsOfSecondsNorth : -tenthsOfSecondsNorth,// eastWest ? degreesEast : -degreesEast,// eastWest ? minutesEast : -minutesEast,// eastWest ? tenthsOfSecondsEast : -tenthsOfSecondsEast// ); } throw new ParseException("Not recognized: " + text, 0); } /** * Matches a latitude expressed with any of the formats for expressing * degrees, minutes, and (optional) seconds (with optional tenths of * seconds). For example <code>32� 18' 23.1" N</code>, * <code>32 18 23.1 N</code> or <code>32:18:23N</code>. * <dl> * <dt>group(2)</dt> * <dd>degrees</dd> * <dt>group(3)</dt> * <dd>minutes</dd> * <dt>group(5)</dt> * <dd>seconds (optional, with optional tenths)</dd> * <dt>group(6)</dt> * <dd>north/south (NnSs)</dd> * </dl> */ static final String regex_lat = // "(" + "(\\d{1,2})\\s?[:�*\\s]?\\s?" + // degrees (0:90) "(\\d{1,2})\\s?[:'\\s]?\\s?" + // minutes "((\\d{1,2}|\\d{1,2}\\.\\d?)\\s?\"?)?\\s?" + // optional seconds // (with optional // tenths) "([NnSs])" + // north/south ")"; /** * Matches a longitude expressed with any of the formats for expressing * degrees, minutes, and (optional) seconds (with optional tenths of * seconds). * * <dl> * <dt>group(2)</dt> * <dd>degrees</dd> * <dt>group(3)</dt> * <dd>minutes</dd> * <dt>group(5)</dt> * <dd>seconds (optional, with optional tenths)</dd> * <dt>group(6)</dt> * <dd>east/west (EeWw)</dd> * </dl> */ static final String regex_long = // "(" + "(\\d{1,3})\\s?[:�*\\s]?\\s?" + // degrees (0:180) "(\\d{1,2})\\s?[:'\\s]?\\s?" + // minutes "((\\d{1,2}|\\d{1,2}\\.\\d?)\\s?\"?)?\\s?" + // optional seconds // (with optional // tenths) "([EeWw])" + // east/west ")"; /** * Matches any of the formats that put separators between degrees, minutes, * and seconds. * <dl> * <dt>group({@link #group_degreesNorth})</dt> * <dd>degrees</dd> * <dt>group({@link #group_minutesNorth})</dt> * <dd>minutes</dd> * <dt>group({@link #group_secondsNorth})</dt> * <dd>seconds</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_secondsEast})</dt> * <dd>seconds</dd> * <dt>group({@link #group_eastWest})</dt> * <dd>east/west (EeWw)</dd> * </dl> * * @see #regex_lat * @see #regex_long */ static final Pattern pattern_dms1 = Pattern.compile("^(" + // "(" + regex_lat + "(\\s?[/,]?\\s?)" + regex_long + ")" + // ")$"// ); /* * The integer identifier of the group in pattern_dms1 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_secondsNorth = 7; static final int group_northSouth = 8; static final int group_degreesEast = 11; static final int group_minutesEast = 12; static final int group_secondsEast = 14; static final int group_eastWest = 15; /** * Formats a value expressing tenths of a second. For example, * <code>328</code> is formatted as <code>32.8</code>. * <p> * Note: This does NOT correct for South or West (negative angles). * * @param tenthsOfSecond * An integer value containing seconds and tenths of a second. * * @return A string representation of that value. */ static String formatTenthsOfSecond(int tenthsOfSecond) { int seconds = tenthsOfSecond / 10; int tenths = tenthsOfSecond - seconds * 10; return seconds + "." + tenths; } /** * Parses a value representing seconds and optional tenths of a second. For * example, <code>32.8</code> is returned as <code>328</code> and * <code>32</code> is returned as <code>320</code>. * * @param text * A representation of seconds and optional tenths of a second. * * @return An integer expressing tenths of a second. */ static int parseTenthsOfSecond(String text) throws ParseException { int pos = text.indexOf("."); int lastPos = text.lastIndexOf("."); if (pos != lastPos) throw new ParseException(text, lastPos); final int tenthsOfSecond; if (pos == -1) { /* * No tenths of a second. */ int seconds = Integer.parseInt(text); tenthsOfSecond = seconds * 10; } else { /* * Tenths of a second are present. */ String secondsStr = text.substring(0, pos); String tenthsStr = text.substring(pos + 1); int seconds = Integer.parseInt(secondsStr); int tenths = tenthsStr.length() == 0 ? 0 : Integer .parseInt(tenthsStr); tenthsOfSecond = seconds * 10 + tenths; } return tenthsOfSecond; } /** * Convert to decimal degrees. * <p> * Decimal degrees = whole number of degrees, plus minutes divided by 60, * plus seconds divided by 3600. */ public CoordinateDD toDD() { final double _degreesNorth = CoordinateUtility.toDecimalDegrees( degreesNorth, minutesNorth, tenthsOfSecondsNorth / 10d); final double _degreesEast = CoordinateUtility.toDecimalDegrees( degreesEast, minutesEast, tenthsOfSecondsEast / 10d); return new CoordinateDD(_degreesNorth, _degreesEast); } public double distance(ICoordinate o, UNITS units) { return CoordinateUtility.distance(toDD(), o.toDD(), units); } public CoordinateDMS toDMS() { return this; } public CoordinateDDM toDDM() { // TODO Auto-generated method stub return null; } }