package com.example.customformatter;
import com.oracle.determinations.web.platform.plugins.PlatformSessionRegisterArgs;
import com.oracle.determinations.web.platform.plugins.PlatformSessionPlugin;
import com.oracle.determinations.engine.Attribute;
import com.oracle.determinations.engine.HaleyType;
import java.text.DecimalFormat;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.util.regex.MatchResult;
/**
* This class extends the PassthroughFormatterPlugin, to treat certain number attributes as latitude values,
* to be formatted like: 28°32'45.63"S
* All other attributes, and non-attribute values, are left to be handled as usual by the superclass's delegate formatter.
*
* On input, the degree, minute, and second marks are optional so users don't need to stare at their keyboard
* looking for the ° key. If the North/South mark is omitted, north latitude is assumed.
*
* Regular numeric format and 'uncertain' values are also accepted (North is positive) and are handled by the superclass's
* delegate formatter.
*/
public class LatitudeFormatterPlugin extends PassthroughFormatterPlugin {
//not static because DecimalFormat is not re-entrant -- good thing the plugin is constructed once per (single-threaded) session!
private DecimalFormat noScientificNotationFormat = new DecimalFormat("0.#####");
//The name of the custom property we are looking for on our attributes
private static final String IS_LATITUDE = "IsLatitude";
//regular expression for matching latitude values -- degree value is mandatory, everything after that is optional.
// Degree/minute/second marks can be left out and spaces used instead.
// No decimal values are permitted except in the seconds' place.
private static final Pattern LATITUDE_PATTERN = Pattern.compile("(\\d+)°?\\s*(\\d*)'?\\s*(\\d*\\.?\\d*)\"?\\s*([nNsS]?)");
/**
* This version of the constructor will be invoked at application startup as a factory.
*/
public LatitudeFormatterPlugin() {
}
/**
* This version of the constructor will be invoked when attaching to a session.
* @param args description of the session
*/
public LatitudeFormatterPlugin(PlatformSessionRegisterArgs args) {
super(args);
}
/**
* Decide whether we want to attach to a given session. This example plugin will attach to every session.
*/
public PlatformSessionPlugin getInstance(PlatformSessionRegisterArgs args) {
return new LatitudeFormatterPlugin(args);
}
/**
* Temporal values are for attributes representing a value at multiple different times
* This plugin can't handle temporal values for latitude, even if the parent can for all other data types,
* So we must return false.
*/
public boolean canFormatTemporal() {
return false;
}
/**
* We overload the formatting method only for attribute values.
* Other versions exist but should not be called where the value is attached to an attribute.
*/
public String getFormattedValue(byte type, Object value, Attribute attr) {
//Attempt to format only if the value type is a number, the attribute has our custom property,
// and the value itself is a normal number value, not unknown or uncertain.
//Otherwise let the default formatter handle it.
if (type == HaleyType.NUMBER && Boolean.parseBoolean(attr.getPropertyValue(IS_LATITUDE, "false")) && value instanceof Number)
return formatLatitude(((Number)value).doubleValue());
else
return super.getFormattedValue(type, value, attr);
}
/**
* We overload the parsing method only for attribute values.
* Other versions exist but should not be called where the value is attached to an attribute.
*/
public Object parse(byte type, String value, Attribute attr) {
//Attempt to parse only if the value type a number and the attribute has our custom property.
//Otherwise let the default formatter handle it.
if (type == HaleyType.NUMBER && Boolean.parseBoolean(attr.getPropertyValue(IS_LATITUDE, "false"))) {
//Even if we do want to parse this value, let the parent try to make sense of it first --
// That way we don't need to worry about getting the uncertain/left blank case right.
Object o;
try {
o = super.parse(type, value, attr);
} catch (RuntimeException e) {
o = parseLatitude(value);
}
if (o instanceof Number && Math.abs(((Number)o).doubleValue()) > 90.)
throw new RuntimeException("Latitude cannot be more than 90 degrees");
return o;
} else {
return super.parse(type, value, attr);
}
}
/**
* Do the actual work of formatting a double into a latitude
*/
private String formatLatitude(double dValue) {
if (Double.isInfinite(dValue) || Double.isNaN(dValue))
throw new RuntimeException("Cannot format illegal latitude " + Double.toString(dValue));
long degrees = (long)dValue; //truncate toward zero; don't allow DecimalFormat to round this part
long minutes = (long)(dValue*60%60); //truncate toward zero; don't allow DecimalFormat to round this part
double seconds = dValue*3600%60; //round our last portion as required
String s = noScientificNotationFormat.format(Math.abs(degrees)) + "°";
if (minutes != 0 || seconds != 0)
s += noScientificNotationFormat.format(Math.abs(minutes)) + "'";
if (seconds != 0)
s += noScientificNotationFormat.format(Math.abs(seconds)) + "\"";
if (degrees != 0 || minutes != 0 || seconds != 0) {
s += dValue > 0 ? "N" : "S";
}
return s;
}
/**
* Do the actual work of parsing a string into a double
*/
private double parseLatitude(String value) {
Matcher matcher = LATITUDE_PATTERN.matcher(value.trim());
if (!matcher.matches())
throw new RuntimeException("String could not be recognized as a latitude: " + value);
//Don't worry about negative values in the numbers, illegal characters, etc. because if they were there our
// regular expression would not have matched.
MatchResult result = matcher.toMatchResult();
String sDegrees = result.group(1);
String sMinutes = result.group(2);
String sSeconds = result.group(3);
String nsMark = result.group(4);
int degrees = Integer.parseInt(sDegrees);
int minutes = 0;
if (sMinutes != null && sMinutes.length() > 0)
minutes = Integer.parseInt(sMinutes);
if (minutes >= 60)
throw new RuntimeException("Number of minutes in an angle must be less than 60.");
double seconds = 0;
if (sSeconds != null && sSeconds.length() > 0)
seconds = Double.parseDouble(sSeconds);
if (seconds >= 60.)
throw new RuntimeException("Number of seconds in an angle must be less than 60.");
double latitude = degrees + (double)minutes/60. + seconds/3600.;
if (nsMark != null && nsMark.length() > 0 && nsMark.toLowerCase().charAt(0) == 's')
latitude *= -1;
return latitude;
}
}