/*
GNU General Public License
CacheWolf is a software for PocketPC, Win and Linux that
enables paperless caching.
It supports the sites geocaching.com and opencaching.de
Copyright (C) 2006 CacheWolf development team
See http://www.cachewolf.de/ for more information.
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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
/*
A parser that parses the following grammar:
EBNF Meta-Symbols:
{xx} xx can occur any number of times incl 0
[xx] xx or empty
| or
"x" x is terminal symbol
command -> if |
simplecommand
simplecommand -> "stop" | "st" |
assign
stringexp |
if -> "IF" stringexpr compop stringexpr "THEN" simplecommand { ";" simplecommand } "ENDIF" // Nested IF's not allowed
compop -> "=" | "<" | ">" | "<=" | "==" | ">=" | "<>" | "!=" | "><"
assign -> ident = [ stringexpr ]
stringexp -> (string | expr ) {string | tailexp }
expr -> ["+" | "-"] tailexp [ formatstring ]
tailexp -> term { ("+" | "-") term }
term -> factor { ("*" | "/") factor }
factor -> expfactor { "^" expfactor }
expfactor -> ident |
number |
"(" stringexpr ")" |
function "(" stringexpr { "," stringexpr }")"
function -> "sin" | "cos" | "tan" | "asin" | "acos" | "atan" | "goto" | "project" | "show" | "crosstotal" |
"rot13" | "len" | "mid"
ident -> valid identifier
number -> valid number
*/
package CacheWolf;
import CacheWolf.database.CWPoint;
import CacheWolf.database.CacheHolder;
import CacheWolf.database.CacheSize;
import CacheWolf.database.CacheTerrDiff;
import CacheWolf.database.CacheType;
import CacheWolf.navi.Navigate;
import CacheWolf.navi.TransformCoordinates;
import CacheWolf.utils.Common;
import CacheWolf.utils.Metrics;
import CacheWolf.utils.MyLocale;
import CacheWolf.utils.STRreplace;
import com.stevesoft.ewe_pat.Regex;
import ewe.sys.Convert;
import ewe.util.Hashtable;
import ewe.util.Iterator;
import ewe.util.Vector;
import ewe.util.mString;
/**
* The wolf language parser. New version - January 2007
*
* New features:
* - Improved error handling
* - Strings and doubles can be freely mixed as appropriate. Depending on context a conversion is performed,
* - Variables can store strings or doubles
* - Global variables (starting with $) are remembered across multiple calls to parser
* - Global variables are initialised with "", local variables result in error if used before setting value
* - IF statement added
* - Many new functions (encode,format,goto,len,mid,count, substring,ucase,lcase,val,sval,replace, reverse,project)
* - less typing
* - Function aliases
* - Function names can be flexibly abbreviated, i.e. instead of crosstotal write cr or cross or crosst ...
* - show no longer needed
* - Command terminator ; no longer compulsory (only between multiple commands on same line)
* - New functions can easily be added
* - Can select whether variable names are case sensitive
*
* To add a new function:
* 1) Add its name and alias and allowed number of args to array functions
* 2) Add a new private method in the "functions" section
* 3) Add call to private method in executeFunction
*
* @author salzkammergut Januay 2007
*/
public class Parser {
private class fnType {
public String funcName; // the function name in the user input
public String alias; // the funcName is mapped to this alias
public int nargs; // bitmap for number of args, i.e. 14 = 1 or 2 or 3 args; 5 = 0 or 2 args
// i.e. 1<<nargs ORed together
fnType(String funcName, String alias, int nargs) {
this.funcName = funcName;
this.alias = alias;
this.nargs = nargs;
}
boolean nargsValid(int testNargs) {
return ((1 << testNargs) & this.nargs) != 0;
}
}
fnType[] functions = new fnType[] { // in alphabetical order
new fnType("abs", "abs", 2), new fnType("acos", "acos", 2), new fnType("asin", "asin", 2), new fnType("atan", "atan", 2), new fnType("bearing", "bearing", 4), new fnType("cb", "cb", 16), new fnType("centre", "center", 3),
new fnType("center", "center", 3), new fnType("cls", "cls", 1), new fnType("clearscreen", "cls", 1), new fnType("cos", "cos", 2), new fnType("count", "count", 4), new fnType("cp", "cp", 1), new fnType("crossbearing", "cb", 16),
new fnType("crosstotal", "ct", 6), new fnType("ct", "ct", 2), new fnType("curpos", "cp", 1), new fnType("d2r", "deg2rad", 2), new fnType("deg", "deg", 1), new fnType("deg2rad", "deg2rad", 2), new fnType("distance", "distance", 4),
new fnType("encode", "encode", 8), new fnType("format", "format", 12), new fnType("goto", "goto", 6), new fnType("ic", "ic", 3), new fnType("ignorecase", "ic", 3), new fnType("instr", "instr", 12), new fnType("int", "int", 2),
new fnType("lcase", "lc", 2), new fnType("length", "len", 2), new fnType("mid", "mid", 12), new fnType("mod", "mod", 4), new fnType("pc", "pz", 3), new fnType("profilecenter", "pz", 3), new fnType("profilecentre", "pz", 3),
new fnType("profilzentrum", "pz", 3), new fnType("project", "project", 8), new fnType("pz", "pz", 3), new fnType("quersumme", "ct", 6), new fnType("r2d", "rad2deg", 2), new fnType("rad", "rad", 1), new fnType("rad2deg", "rad2deg", 2),
new fnType("replace", "replace", 8), new fnType("reverse", "reverse", 2), new fnType("rot13", "rot13", 2), new fnType("show", "show", 2), new fnType("sin", "sin", 2), new fnType("skeleton", "skeleton", 3), new fnType("sqrt", "sqrt", 2),
new fnType("sval", "sval", 2), new fnType("tolowercase", "lc", 2), new fnType("touppercase", "uc", 2), new fnType("tan", "tan", 2), new fnType("ucase", "uc", 2), new fnType("val", "val", 2), new fnType("zentrum", "center", 3) };
private static int scanpos = 0;
CWPoint cwPt = new CWPoint();
Vector calcStack = new Vector();
Hashtable symbolTable = new Hashtable(50);
TokenObj thisToken = new TokenObj();
Vector tokenStack;
Vector messageStack;
public Parser() { // Global constructor
}
// /////////////////////////////////////////
// Utility functions
// /////////////////////////////////////////
/*
* All errors are handled via function 'err'. Rather than creating many different Exceptions,
* only the standard Exception is used. err raises this exception and thereby causes the stack to be
* unwound until 'parse' eventually catches the exception and returns to SolverPanel, which displays
* the messageStack containing the error message.
*/
/**
* Add an error message to the message stack and raise an Exception.
*/
private void err(String str) throws Exception {
messageStack.add(MyLocale.getMsg(1700, "Error on line: ") + thisToken.line + " " + MyLocale.getMsg(1701, "position: ") + thisToken.position);
messageStack.add(str);
// move cursor to error location
MainTab.itself.solverPanel.setSelectionRange(0, thisToken.line - 1, thisToken.position + thisToken.token.length() - 1, thisToken.line - 1);
throw new Exception("Error " + str);
}
/** Shows global symbols */
private void showVars(boolean globals) throws Exception {
Iterator it = symbolTable.entries();
while (it.hasNext()) {
String varName = ((String) ((ewe.util.Map.MapEntry) it.next()).getKey());
if (globals == varName.startsWith("$")) {
String value = (String) getVariable(varName);
if (java.lang.Double.isNaN(toNumber(value)))
messageStack.add(varName + " = \"" + STRreplace.replace(value.toString(), "\"", "\"\"") + "\"");
else
messageStack.add(varName + " = " + value);
}
}
}
/** Clears the symbol table of all non-global symbols (those not starting with $) */
private void clearLocalSymbols() {
Iterator it = symbolTable.entries();
while (it.hasNext()) {
ewe.util.Map.MapEntry sym = (ewe.util.Map.MapEntry) it.next();
if (!((String) sym.getKey()).startsWith("$"))
symbolTable.remove(sym.getKey());
}
Double pi = new Double(java.lang.Math.PI);
symbolTable.put("PI", pi);
symbolTable.put("pi", pi); // To make it easier for the user we also add a lowercase version of pi
}
private boolean isVariable(String varName) {
return varName.startsWith("$") || // Global variables exist per default
symbolTable.containsKey(Preferences.itself().solverIgnoreCase ? varName.toUpperCase() : varName);
}
private boolean isInteger(double d) {
return java.lang.Math.ceil(d) == d && java.lang.Math.floor(d) == d;
}
private boolean isValidCoord(String coord) {
cwPt.set(coord);
return cwPt.isValid();
}
private Object getVariable(String varName) throws Exception {
if (varName.startsWith("$")) { // Potential coordinate
CacheHolder ch = MainForm.profile.cacheDB.get(varName.substring(1));
if (ch != null) { // Found it!
// Check whether coordinates are valid
cwPt.set(ch.getWpt());
if (cwPt.isValid())
return cwPt.toString();
else
return ""; // Convert invalid coordinates (N 0 0.0 E 0 0.0) into empty string
}
}
Object result = symbolTable.get(Preferences.itself().solverIgnoreCase ? varName.toUpperCase() : varName);
if (result == null) {
// If it is a global variable, add it with a default value
if (varName.startsWith("$")) {
result = "";
symbolTable.put(Preferences.itself().solverIgnoreCase ? varName.toUpperCase() : varName, "");
} else
err(MyLocale.getMsg(1702, "Variable not defined: ") + varName);
}
return result;
}
private double toNumber(String str) {
try {
if (Common.getDigSeparator() == ',')
str = str.replace('.', ',');
else
str = str.replace(',', '.');
return java.lang.Double.parseDouble(str);
} catch (NumberFormatException e) {
return java.lang.Double.NaN;
}
}
private Double getNumber(String str) throws Exception {
double ret = toNumber(str);
if (java.lang.Double.isNaN(ret))
err(MyLocale.getMsg(1703, "Not a valid number: ") + str);
return new java.lang.Double(ret);
}
/** Get the top element of the calculation stack and try and convert it to a number if it is a string */
private double popCalcStackAsNumber(double defaultForEmptyString) throws Exception {
double num;
if (calcStack.get(calcStack.size() - 1) instanceof String) {
if (((String) calcStack.get(calcStack.size() - 1)).equals(""))
num = defaultForEmptyString;
else
num = getNumber((String) calcStack.get(calcStack.size() - 1)).doubleValue();
} else {
num = ((java.lang.Double) calcStack.get(calcStack.size() - 1)).doubleValue();
}
calcStack.removeElementAt(calcStack.size() - 1);
return num;
}
private String popCalcStackAsString() {
String s;
if (calcStack.get(calcStack.size() - 1) instanceof Double) {
java.lang.Double D = ((java.lang.Double) calcStack.get(calcStack.size() - 1));
// Double.toString() formats numbers > 1E7 and < 1E-3 with exponential notation
// For large integers we therefore use Longs
double d = D.doubleValue();
// If the double is an integer and within range of longs, use Long
if (java.lang.Math.floor(d) == d && d < java.lang.Long.MAX_VALUE && d > java.lang.Long.MIN_VALUE) {
java.lang.Long L = new java.lang.Long((long) d);
s = L.toString();
} else { // Use the default Double format
s = D.toString().replace(',', '.'); // always show numbers with decimal point;
if (s.endsWith(".0"))
s = s.substring(0, s.length() - 2);
}
} else
s = (String) calcStack.get(calcStack.size() - 1);
calcStack.removeElementAt(calcStack.size() - 1);
return s;
}
private void getToken() throws Exception {
if (scanpos < tokenStack.size()) {
thisToken = (TokenObj) tokenStack.get(scanpos);
scanpos++;
} else
err(MyLocale.getMsg(1704, "Unexpected end of source"));
}
private TokenObj peekToken() {
if (scanpos < tokenStack.size()) {
return (TokenObj) tokenStack.get(scanpos);
} else
return new TokenObj();
}
private void getNextTokenOtherThanSemi() throws Exception {
do {
getToken();
} while (thisToken.token.equals(";"));
}
private void skipPastEndif(TokenObj ifToken) throws Exception {
while (scanpos < tokenStack.size()) {
thisToken = (TokenObj) tokenStack.get(scanpos);
scanpos++;
if (thisToken.tt == TokenObj.TT_ENDIF) {
getToken();
return;
}
}
thisToken = ifToken;
err(MyLocale.getMsg(1705, "Missing ENDIF"));
}
private TokenObj lookAheadToken() {
return (TokenObj) tokenStack.get(scanpos);
}
private boolean checkNextSymIs(String str) throws Exception {
if (thisToken.token.toUpperCase().equals(str)) {
return true;
} else {
err(MyLocale.getMsg(1706, "Expected ") + str + " " + MyLocale.getMsg(1707, "Found: ") + thisToken.token);
return false; // Dummy as err does not return
}
}
private fnType getFunctionDefinition(String str) throws Exception {
fnType fnd = null;
str = str.toLowerCase();
for (int i = functions.length - 1; i >= 0; i--) {
// Return the function if there is an exact match
if (functions[i].funcName.equals(str))
return functions[i];
if (functions[i].funcName.startsWith(str)) { // Partial match?
// Only one partial match allowed
if (fnd != null)
err(MyLocale.getMsg(1708, "Ambiguous function name: ") + str);
fnd = functions[i];
}
}
if (fnd == null)
err(MyLocale.getMsg(1709, "Unknown function: ") + str);
return fnd;
}
// /////////////////////////////////////////
// FUNCTIONS
// /////////////////////////////////////////
/** If we are in DEGree mode, convert the argument to RADiants, if not leave it unchanged */
private double makeRadiant(double arg) {
if (Preferences.itself().solverDegMode)
return arg * java.lang.Math.PI / 180.0;
else
return arg;
}
/** If we are in DEGree mode, convert the argument to degrees */
private double makeDegree(double arg) {
if (Preferences.itself().solverDegMode)
return arg / java.lang.Math.PI * 180.0;
else
return arg;
}
/** Calculate brearing from one point to the next */
private double funcBearing() throws Exception {
String coordB = popCalcStackAsString();
String coordA = popCalcStackAsString();
if (!isValidCoord(coordA))
err(MyLocale.getMsg(1712, "Invalid coordinate: ") + coordA);
if (!isValidCoord(coordB))
err(MyLocale.getMsg(1712, "Invalid coordinate: ") + coordB);
cwPt.set(coordA);
double angleDeg = cwPt.getBearing(new CWPoint(coordB));
// getBearing returns a result in degrees
return Preferences.itself().solverDegMode ? angleDeg : angleDeg * java.lang.Math.PI / 180.0;
}
/** Get or set the current centre */
private void funcCenter(int nargs) throws Exception {
if (nargs == 0) {
calcStack.add(Preferences.itself().curCentrePt.toString());
} else {
String coordA = popCalcStackAsString();
if (!isValidCoord(coordA))
err(MyLocale.getMsg(1712, "Invalid coordinate: ") + coordA);
MainForm.itself.setCurCentrePt(new CWPoint(coordA));
}
}
/** Clear Screen */
private void funcCls() {
MainTab.itself.solverPanel.cls();
}
private int funcCountChar(String s, char c) {
int count = 0;
for (int i = 0; i < s.length(); i++)
if (s.charAt(i) == c)
count++;
return count;
}
/**
* count(string1,string2)
* */
private void funcCount() throws Exception {
String s2 = popCalcStackAsString();
String s1 = popCalcStackAsString();
if (s2.length() == 0)
err(MyLocale.getMsg(1710, "Cannot count empty string"));
if (s2.length() == 1) {
calcStack.add(new Double(funcCountChar(s1, s2.charAt(0))));
} else {
String res = "";
for (int i = 0; i < s2.length(); i++) {
res += s2.charAt(i) + "=" + funcCountChar(s1, s2.charAt(i)) + " ";
}
calcStack.add(res);
}
}
private String funcCp() {
return Navigate.gpsPos.toString();
}
/**
* Crosstotal: Works for both strings and numbers. For strings any non-numeric character is ignored
* Warning: When the number is non-integer or > 9223372036854775807, it is formatted using the E
* notation, i.e. x.xxxxxxEyy. In this case the exponent yy is also included in the crosstotal
*/
private double funcCrossTotal(int nargs) throws Exception {
int cycles = 1;
if (nargs == 2)
cycles = (int) popCalcStackAsNumber(1);
String aString = popCalcStackAsString().replace('-', '0').trim();
double a = 0;
if (cycles < 0)
cycles = 1;
if (cycles > 5)
cycles = 5;
while (cycles-- > 0) {
// Cross total = Quersumme berechnen
a = 0;
for (int i = 0; i < aString.length(); i++) {
if (aString.charAt(i) >= '0' && aString.charAt(i) <= '9')
a += aString.charAt(i) - '0';
}
aString = Convert.toString(a);
}
return a;
}
private void funcDeg(boolean arg) {
Preferences.itself().solverDegMode = arg;
MainTab.itself.solverPanel.showSolverMode();
}
/** Convert degrees into Radiants */
private double funcDeg2Rad() throws Exception {
double a = popCalcStackAsNumber(0);
return a / 180.0 * java.lang.Math.PI;
}
/** Calculate distance between 2 points */
private double funcDistance() throws Exception {
String coordB = popCalcStackAsString();
String coordA = popCalcStackAsString();
double result = 0;
// Attention: isValidCoord has sideeffect of setting cwPt
if (!isValidCoord(coordA))
err(MyLocale.getMsg(1712, "Invalid coordinate: ") + coordA);
if (!isValidCoord(coordB))
err(MyLocale.getMsg(1712, "Invalid coordinate: ") + coordB);
cwPt.set(coordA);
double distKM = cwPt.getDistance(new CWPoint(coordB));
result = distKM * 1000.0;
if (Preferences.itself().metricSystem == Metrics.IMPERIAL) {
result = Metrics.convertUnit(distKM, Metrics.KILOMETER, Metrics.YARDS);
}
return result;
}
/**
* Encode a string by replacing all characters in a string with their corresponding characters in
* another string
*
* @throws Exception
*/
private String funcEncode() throws Exception {
String newChars = popCalcStackAsString();
String oldChars = popCalcStackAsString();
if (newChars.length() != oldChars.length())
err(MyLocale.getMsg(1711, "Replacement characters strings must be of equal length"));
String s = popCalcStackAsString();
String encodedStr = "";
for (int i = 0; i < s.length(); i++) {
int pos;
if ((pos = oldChars.indexOf(s.charAt(i))) != -1) {
encodedStr += newChars.charAt(pos);
} else
encodedStr += s.charAt(i);
}
return encodedStr;
}
/**
* Format a valid coordinate
* If called with one args, format the argument on the stack to CW standard
* The optional second argument is one of these strings "UTM","DMS","DD","DMM" or "CW"
*
* @param nargs
* 1 or 2 args
*/
private String funcFormat(int nargs) throws Exception {
int spart = 0;
if (nargs == 3)
spart = (int) popCalcStackAsNumber(0);
String fmtStr = "";
if (nargs >= 2)
fmtStr = popCalcStackAsString().toLowerCase();
String coord = popCalcStackAsString();
if (!isValidCoord(coord))
err(MyLocale.getMsg(1712, "Invalid coordinate: ") + coord);
cwPt.set(coord);
int fmt = TransformCoordinates.getLocalSystemCode(fmtStr);
if (fmt == TransformCoordinates.LOCALSYSTEM_NOT_SUPPORTED)
err(MyLocale.getMsg(1713, "Invalid coordinate format. Allowed are cw / dd / dmm / dms / ") + Common.arrayToString(TransformCoordinates.getProjectedSystemIDs(), " / "));
String ret = cwPt.toString(fmt);
if (nargs == 3) {
String[] parts = mString.split(ret, ' ');
if (spart > 0 && parts.length >= spart)
ret = parts[spart - 1];
else
err("Param 3 !!! " + MyLocale.getMsg(1713, "Invalid coordinate format."));
}
return ret;
}
/**
* Implements a goto command goto(coordinate,optionalWaypointName).
*/
private void funcGoto(int nargs) throws Exception {
Navigate nav = MainTab.itself.navigate;
String waypointName = null;
if (nargs == 2)
waypointName = popCalcStackAsString();
String coord = popCalcStackAsString();
if (!isValidCoord(coord))
err(MyLocale.getMsg(1712, "Invalid coordinate: ") + coord);
// Don't want to switch to goto panel, just set the values
nav.setDestination(coord);
if (nargs == 2) { // Now set the value of the addi waypoint (it must exist already)
cwPt.set(coord);
CacheHolder ch = MainForm.profile.cacheDB.get(waypointName);
if (ch == null) {
err(MyLocale.getMsg(1714, "Goto: Waypoint does not exist: ") + waypointName);
return;
}
ch.setWpt(cwPt);
ch.calcDistance(Preferences.itself().curCentrePt); // Update distance/bearing
nav.setDestination(ch);
MainForm.profile.selectionChanged = true; // Tell moving map to updated displayed waypoints
}
}
/** Display or change the case sensitivity of variable names */
private void funcIgnoreVariableCase(int nargs) throws Exception {
if (nargs == 0)
calcStack.add("" + Preferences.itself().solverIgnoreCase);
else {
Preferences.itself().solverIgnoreCase = (popCalcStackAsNumber(0) != 0) ? true : false;
}
}
/**
* VB instr function
* instr([start],string1,string2)
* */
private int funcInstr(int nargs) throws Exception {
String s2 = popCalcStackAsString();
String s1 = popCalcStackAsString();
int start = 1;
if (nargs == 3)
start = (int) popCalcStackAsNumber(1);
if (start > s1.length())
err(MyLocale.getMsg(1715, "instr: Start position not in string"));
if (s2.equals("")) {
if (s1.equals(""))
return 0;
else
return 1;
}
return s1.indexOf(s2, start - 1) + 1;
}
/** MID function as in Basic */
private String funcMid(int nargs) throws Exception {
if (nargs == 2) {
double start = popCalcStackAsNumber(0);
String s = popCalcStackAsString();
if (!isInteger(start))
err(MyLocale.getMsg(1716, "mid: Integer argument expected"));
if (start < 1 || start > s.length())
err(MyLocale.getMsg(1717, "mid: Argument out of range"));
return s.substring((int) start - 1);
} else {
double len = popCalcStackAsNumber(0);
double start = popCalcStackAsNumber(0);
String s = popCalcStackAsString();
if (!isInteger(start) || !isInteger(len))
err(MyLocale.getMsg(1716, "mid: Integer argument expected"));
int end = (int) (start + len - 1);
if (start > s.length() || start < 1 || end > s.length())
err(MyLocale.getMsg(1717, "mid: Argument out of range"));
return s.substring((int) start - 1, end);
}
}
/** MOD function as in Basic */
private Double funcMod() throws Exception {
double b = popCalcStackAsNumber(0);
double a = popCalcStackAsNumber(0);
if (b == 0.0)
err(MyLocale.getMsg(1729, "Division by 0"));
return new java.lang.Double(a % b);
}
/** Get or set the profile centre */
private void funcPz(int nargs) throws Exception {
if (nargs == 0) {
calcStack.add(MainForm.profile.center.toString());
} else {
String coordA = popCalcStackAsString();
if (!isValidCoord(coordA))
err(MyLocale.getMsg(1712, "Invalid coordinate: ") + coordA);
MainForm.profile.center.set(coordA);
}
}
/**
* Calculates the crossbearing from point1 with bearing 1 and point2 with bearing2
* point1 and point 2 must be different.
* Not very well tested. No guarantee for correct result if any of the distance is greater than 300 kilometers and / or any of the angles in the spherical triangle id greater then 90degrees
*
* @return
* @throws Exception
*/
private String funcCrossBearing() throws Exception {
// parameters come in reversed order!
double degrees2 = popCalcStackAsNumber(-1);
String coordinates2 = popCalcStackAsString();
double degrees1 = popCalcStackAsNumber(-1);
String coordinates1 = popCalcStackAsString();
if (!isValidCoord(coordinates1))
err(MyLocale.getMsg(1712, "Invalid coordinate: ") + coordinates1);
if (!isValidCoord(coordinates2))
err(MyLocale.getMsg(1712, "Invalid coordinate: ") + coordinates2);
// Check parameters: Range
if (degrees1 < 0 || degrees1 > 360 || degrees2 < 0 || degrees2 > 360) {
if (Preferences.itself().solverDegMode) {
err(MyLocale.getMsg(1740, "Crossbearing degrees must be in interval [0;360]"));
} else {
err(MyLocale.getMsg(1741, "Crossbearing degrees must be in interval [0;2*PI]"));
}
}
double rAN = Preferences.itself().solverDegMode ? degrees1 / 180.0 * java.lang.Math.PI : degrees1;
double rBN = Preferences.itself().solverDegMode ? degrees2 / 180.0 * java.lang.Math.PI : degrees2;
CWPoint point1 = new CWPoint(coordinates1);
CWPoint point2 = new CWPoint(coordinates2);
// check Parameters: bearings to project must be different from the bearing between point1 and point2
if (degrees1 == degrees2) {
double bearing1 = point1.getBearing(point2);
double bearing2 = point1.getBearing(point1);
if (bearing1 == degrees1 || bearing2 == degrees2) {
err(MyLocale.getMsg(1740, "Invalid crossbearing angles"));
}
}
CWPoint result2 = crossbearingCalculation(point1, point2, rAN, rBN);
return result2.toString();
}
private CWPoint crossbearingCalculation(CWPoint point1, CWPoint point2, double rAN, double rBN) throws Exception {
// see german wikipedia keyword vorwaertsschnitt for the calculation.
// peilung von a->b
// Yes we will make an error, therefore we have to calculate the target-point iteratively.
// Testcode for crossbearing:
// MP="S35 47.100 W089 43.200" # MP is centre of circle, could be any waypoint
// A=project(MP,0,1000); B=project(MP,120,1000) # Points of equilateral triangle on circle
// C1=project(MP,240,1000); C2=cb(A,210 ,B,270)
// C1 "=" C2
final int maxRadius = 6378;
double distance = point1.getDistance(point2);
if (Math.abs(distance) <= 0.0000000001) {
err(MyLocale.getMsg(1742, "Crossbearing: distance between points to small"));
}
double distanceInRad = distance / maxRadius;
double phiAB = point1.getBearing(point2);
if (Preferences.itself().solverDegMode)
phiAB = phiAB / 180.0 * java.lang.Math.PI;
double phiBA = point2.getBearing(point1);
if (Preferences.itself().solverDegMode)
phiBA = phiBA / 180.0 * java.lang.Math.PI;
double psi = phiAB - rAN;
double phi = rBN - phiBA;
// calculate projetiondistance
double bInRad = distanceInRad * java.lang.Math.sin(phi) / java.lang.Math.sin(phi + psi);
double b = bInRad * maxRadius;// * (1-flattening);
double aInRad = distanceInRad * java.lang.Math.sin(psi) / java.lang.Math.sin(phi + psi);
double a = aInRad * maxRadius;// * (1-flattening);
double phiAN = phiAB - psi;
double phiANDegrees = phiAN * 180.0 / java.lang.Math.PI;
double phiBN = phiBA + phi;
double phiBNDegrees = phiBN * 180.0 / java.lang.Math.PI;
CWPoint result2 = point2.project(phiBNDegrees, a);
CWPoint result = point1.project(phiANDegrees, b);
double errorDistance = result.getDistance(result2);
// if the distance between the points is to large, we will restart the calculation with the new points found.
// since the error is mostly very small these iterations are seldom used and the needed depth is very low.
// First we will make sure, that this calculation will terminate
if (distance < errorDistance) {
err(MyLocale.getMsg(1743, "Crossbearing calculation failed. Please inform the developers at geoclub.de"));
}
if (errorDistance * 1000 > 1) {
return crossbearingCalculation(result, result2, rAN, rBN);
}
return result2;
}
/** Project a waypoint at some angle and some distance */
private String funcProject() throws Exception {
double distance = popCalcStackAsNumber(0);
if (distance < 0)
err(MyLocale.getMsg(1718, "Cannot project a negative distance"));
double degrees = popCalcStackAsNumber(0);
// If we are not in degree mode, arg is in radiants ==> convert it
if (!Preferences.itself().solverDegMode)
degrees = degrees * 180.0 / java.lang.Math.PI;
if (degrees < 0 || degrees > 360)
if (Preferences.itself().solverDegMode)
err(MyLocale.getMsg(1719, "Projection degrees must be in interval [0;360]"));
else
err(MyLocale.getMsg(1739, "Projection degrees must be in interval [0;2*PI]"));
String coord = popCalcStackAsString();
if (!isValidCoord(coord))
err(MyLocale.getMsg(1712, "Invalid coordinate: ") + coord);
cwPt.set(coord);
if (Preferences.itself().metricSystem == Metrics.IMPERIAL) {
distance = Metrics.convertUnit(distance, Metrics.YARDS, Metrics.KILOMETER);
} else {
distance = distance / 1000.0;
}
return cwPt.project(degrees, distance).toString();
}
/** Convert Radiants into degrees */
private double funcRad2Deg() throws Exception {
double a = popCalcStackAsNumber(0);
return a * 180.0 / java.lang.Math.PI;
}
/** Replace all occurrences of a string with another string */
private String funcReplace() throws Exception {
String replaceWith = popCalcStackAsString();
String whatToReplace = popCalcStackAsString();
String s = popCalcStackAsString();
if (whatToReplace.equals(""))
return s;
return STRreplace.replace(s, whatToReplace, replaceWith);
}
/** Reverse a string */
private String funcReverse(String s) {
String res = "";
for (int i = s.length() - 1; i >= 0; i--)
res += s.charAt(i);
return res;
}
/**
* Create a skeleton for multis. This function can be called in three ways:<br>
*
* <pre>
* sk() Create skeleton for current cache (must have addi wpts)
* sk(number) Create skeleton for number variables
*/
private void funcSkeleton(int nargs) throws Exception {
String waypointName = MainTab.itself.lastselected;
CacheHolder c = MainForm.profile.cacheDB.get(waypointName);
if (c == null)
return;
// If it is an addi, find its main cache
if (c.isAddiWpt()) {
waypointName = c.mainCache.getCode();
}
int nStages = -1;
if (nargs == 1) {
nStages = (int) popCalcStackAsNumber(-1.0);
}
// Remove the sk command from the instructions
Regex rex = new Regex("sk\\(.*?\\)", "");
MainTab.itself.solverPanel.setText(rex.replaceFirst(MainTab.itself.solverPanel.getInstructions()));
StringBuffer op = new StringBuffer(1000);
// Check for sk(number)
if (nStages > 0 && nStages < 30) { // e.g. sk(3)
/*
* IF $01xxxx="" THEN
* $01xxxx=""
* "Station 1 = " $01xxxx
* goto($01xxxx); STOP
* ENDIF
*/
boolean didCreateWp = false;
for (int i = 0; i < nStages; i++) {
String stage = MyLocale.formatLong(i, "00");
String stageWpt = "$" + stage + waypointName.substring(2);
String stageName = "Stage " + (i + 1);
byte type = CacheType.CW_TYPE_STAGE;
if (i == nStages - 1) {
stageName = "Final";
type = CacheType.CW_TYPE_FINAL;
}
didCreateWp |= createWptIfNeeded(stage + waypointName.substring(2), stageName, type);
op.append("IF " + stageWpt + "=\"\" THEN\n");
op.append(" " + stageWpt + " = \"\"\n");
op.append(" \"" + stageName + " = \" " + stageWpt + "\n");
op.append(" goto(" + stageWpt + "); STOP\n");
op.append("ENDIF\n");
}
MainTab.itself.solverPanel.appendText(op.toString(), true);
if (didCreateWp) {
MainTab.itself.updatePendingChanges();
MainTab.itself.tablePanel.refreshTable();
}
} else {
CacheHolder ch = MainForm.profile.cacheDB.get(waypointName);
if (ch == null) {
err(MyLocale.getMsg(1714, "Goto: Waypoint does not exist: ") + waypointName);
return;
}
CacheHolder addiWpt;
if (ch.hasAddiWpt()) {
op.append("cls()\n");
for (int j = 0; j < ch.addiWpts.getCount(); j++) {
addiWpt = (CacheHolder) ch.addiWpts.get(j);
op.append("IF $");
op.append(addiWpt.getCode());
op.append("=\"\" THEN\n $");
op.append(addiWpt.getCode());
op.append("=\"\"");
// op.append(addiWpt.pos.toString());
op.append("\n \"Punkt ");
op.append(addiWpt.getCode().substring(0, 2));
op.append(" [");
op.append(addiWpt.getName());
op.append("] = \" $");
op.append(addiWpt.getCode());
if (addiWpt.getDetails().LongDescription.trim().length() > 0)
op.append("\n \"" + STRreplace.replace(addiWpt.getDetails().LongDescription, "\"", "\"\"") + "\"");
op.append("\n goto($");
op.append(addiWpt.getCode());
op.append("); STOP\nENDIF\n\n");
}
MainTab.itself.solverPanel.appendText(op.toString(), true);
}// if hasAddiWpt
}
}
private double funcSqrt() throws Exception {
double a = popCalcStackAsNumber(0);
if (a < 0)
err(MyLocale.getMsg(1720, "Cannot calculate square root of a negative number"));
return java.lang.Math.sqrt(a);
}
/** Replace each character by its number A=1, B=2 etc. and put result into a string */
private String funcSval(String s) {
s = s.toLowerCase();
String res = "";
for (int i = 0; i < s.length(); i++) {
int pos = "abcdefghijklmnopqrstuvwxyz".indexOf(s.charAt(i));
if (pos >= 0)
res += (res.length() == 0 ? "" : " ") + MyLocale.formatLong(pos + 1, "00");
}
return res;
}
/** Replace each character by its number A=1, B=2 etc. and sum them */
private double funcVal(String s) {
s = s.toLowerCase();
int sum = 0;
for (int i = 0; i < s.length(); i++) {
sum += "abcdefghijklmnopqrstuvwxyz".indexOf(s.charAt(i)) + 1;
}
return sum;
}
// /////////////////////////////////////////
// PARSER
// /////////////////////////////////////////
/**
* The following methods implement a recursive descent parser.
* Each method is called with 'thisToken' containing a valid token. It must return with 'thisToken' again containing
* a valid token.
*/
private void parseCommand() throws Exception {
while (scanpos < tokenStack.size()) {
getToken();
if (thisToken.token.equals(";"))
continue; // skip an empty command
if (thisToken.tt == TokenObj.TT_IF)
parseIf();
else
parseSimpleCommand();
checkNextSymIs(";");
}
}
private void parseSimpleCommand() throws Exception {
if (thisToken.tt == TokenObj.TT_STOP)
throw new Exception("STOP"); // Terminate without error message
if (thisToken.token.equals("$")) { // Show all global variables
showVars(true);
getToken();
} else if (thisToken.token.equals("?")) { // Show all local variables
showVars(false);
getToken();
} else if (thisToken.tt == TokenObj.TT_VARIABLE && lookAheadToken().tt == TokenObj.TT_EQ)
parseAssign();
else {
parseStringExp();
while (calcStack.size() > 0)
messageStack.add(popCalcStackAsString());
}
}
private void parseIf() throws Exception {
int compOp;
boolean compRes = false;
TokenObj ifToken = thisToken;
getToken();
// Check for "IF varName THEN" construct to check whether a variable is defined
if (thisToken.tt == TokenObj.TT_VARIABLE && peekToken().token.toUpperCase().equals("THEN")) {
String varName = thisToken.token;
getToken(); // THEN
Object result = symbolTable.get(Preferences.itself().solverIgnoreCase ? varName.toUpperCase() : varName);
if (result == null) { // Var not found check whether it is a waypoint
if (varName.startsWith("$")) { // Could be a cachename
varName = varName.substring(1);
compRes = MainForm.profile.getCacheIndex(varName) != -1;
} else
compRes = false;
} else
// Found the variable, it must have a value
compRes = true;
getNextTokenOtherThanSemi();
} else { // Normal: IF expression THEN
parseStringExp();
compOp = thisToken.tt;
if (compOp < TokenObj.TT_LT || compOp > TokenObj.TT_NE)
err(MyLocale.getMsg(1723, "Comparison operator expected"));
getToken();
parseStringExp();
checkNextSymIs("THEN");
getNextTokenOtherThanSemi();
boolean compAsString = false; // calcStack.get(calcStack.size()-2) instanceof String;
// If we can parse the first argument as a double, we will do a numeric comparison
try {
Common.parseDoubleException((String) calcStack.get(calcStack.size() - 2));
} catch (Exception ex) {
compAsString = true;
}
// If the first expression is not a double, compare as string.
if (compAsString) {
String b = popCalcStackAsString();
String a = popCalcStackAsString();
switch (compOp) {
case TokenObj.TT_EQ:
compRes = a.equals(b);
break;
case TokenObj.TT_NE:
compRes = !a.equals(b);
break;
case TokenObj.TT_LT:
compRes = a.compareTo(b) < 0;
break;
case TokenObj.TT_GT:
compRes = a.compareTo(b) > 0;
break;
case TokenObj.TT_LE:
compRes = a.compareTo(b) <= 0;
break;
case TokenObj.TT_GE:
compRes = a.compareTo(b) >= 0;
break;
}
} else { // First expression is a number, compare as numbers
double b = popCalcStackAsNumber(0);
double a = popCalcStackAsNumber(0);
switch (compOp) {
case TokenObj.TT_EQ:
compRes = a == b;
break;
case TokenObj.TT_NE:
compRes = a != b;
break;
case TokenObj.TT_LT:
compRes = a < b;
break;
case TokenObj.TT_GT:
compRes = a > b;
break;
case TokenObj.TT_LE:
compRes = a <= b;
break;
case TokenObj.TT_GE:
compRes = a >= b;
break;
}
}
}
if (compRes) { // comparison resulted in TRUE
if (thisToken.tt != TokenObj.TT_ENDIF) {
parseSimpleCommand();
while (thisToken.token.equals(";")) {
getNextTokenOtherThanSemi(); // Now we have either an ENDIF or the start of a simpleexpression
if (thisToken.tt == TokenObj.TT_ENDIF)
break;
parseSimpleCommand();
}
checkNextSymIs("ENDIF");
}
getToken();
} else
// comparison failed
skipPastEndif(ifToken);
}
private void parseAssign() throws Exception {
String varName = thisToken.token;
getToken(); // =
getToken();
// Assigns of the format A=; are ignored so that they can stay as placeholders and
// we can fill the data progressively during a multicache
if (thisToken.tt == TokenObj.TT_ENDIF || thisToken.token.equals(";"))
return;
parseStringExp();
if (varName.startsWith("$")) { // Potential coordinate
CacheHolder ch = MainForm.profile.cacheDB.get(varName.substring(1));
if (ch != null) { // Yes, is a coordinate
// Check whether new coordinates are valid
String coord = popCalcStackAsString();
cwPt.set(coord);
if (cwPt.isValid() || coord.equals("")) { // Can clear coord with empty string
ch.setWpt(cwPt);
ch.calcDistance(Preferences.itself().curCentrePt); // Update distance and bearing
MainForm.profile.selectionChanged = true; // Tell moving map to updated displayed waypoints
return;
} else
err(MyLocale.getMsg(1712, "Invalid coordinate: ") + coord);
}
// Name starts with $ but is not a waypoint, fall through and set it as global variable
}
symbolTable.put(Preferences.itself().solverIgnoreCase ? varName.toUpperCase() : varName, popCalcStackAsString());
}
private void parseStringExp() throws Exception {
if (thisToken.tt == TokenObj.TT_STRING) {
calcStack.add(thisToken.token);
getToken();
} else {
parseExp();
}
// calcStack.add(popCalcStackAsString());
while (thisToken.tt == TokenObj.TT_STRING || thisToken.tt == TokenObj.TT_NUMBER || thisToken.tt == TokenObj.TT_VARIABLE || thisToken.tt == TokenObj.TT_SYMBOL && thisToken.token.equals("(")) {
if (thisToken.tt == TokenObj.TT_STRING) {
calcStack.add(thisToken.token);
getToken();
} else {
parseTailExp('+');
}
String b = popCalcStackAsString();
String a = popCalcStackAsString();
calcStack.add(a + b);
}
}
private void parseExp() throws Exception {
char unaryOp = '+';
if (thisToken.token.equals("+") || thisToken.token.equals("-")) {
unaryOp = thisToken.token.charAt(0);
getToken();
}
parseTailExp(unaryOp);
}
private void parseTailExp(char unaryOp) throws Exception {
parseTerm();
if (unaryOp == '-') { // Unary minus, negate the first term
calcStack.add(new java.lang.Double(-popCalcStackAsNumber(0)));
}
while (thisToken.token.equals("+") || thisToken.token.equals("-")) {
char op = thisToken.token.charAt(0);
getToken();
parseTerm();
double b = popCalcStackAsNumber(0);
double a = popCalcStackAsNumber(0);
if (op == '+')
calcStack.add(new java.lang.Double(a + b));
else
calcStack.add(new java.lang.Double(a - b));
}
// If expression is followed by a formatstring, format it
if (thisToken.tt == TokenObj.TT_FORMATSTR) {
calcStack.add(MyLocale.formatDouble(popCalcStackAsNumber(0), thisToken.token).replace(',', '.'));
getToken();
}
}
private void parseTerm() throws Exception {
parseFactor();
while (thisToken.token.equals("*") || thisToken.token.equals("/")) {
char op = thisToken.token.charAt(0);
getToken();
parseFactor();
double b = popCalcStackAsNumber(1);
double a = popCalcStackAsNumber(1);
if (op == '*')
calcStack.add(new java.lang.Double(a * b));
else if (b == 0.0)
err(MyLocale.getMsg(1729, "Division by 0"));
else
calcStack.add(new java.lang.Double(a / b));
}
}
private void parseFactor() throws Exception {
parseExpFactor();
while (thisToken.token.equals("^")) {
getToken();
parseExpFactor();
double exp = popCalcStackAsNumber(0);
double base = popCalcStackAsNumber(0);
calcStack.add(new java.lang.Double(java.lang.Math.pow(base, exp)));
}
}
private void parseExpFactor() throws Exception {
fnType funcDef;
if (thisToken.tt == TokenObj.TT_VARIABLE) {
if (isVariable(thisToken.token) && !lookAheadToken().token.equals("("))
calcStack.add(getVariable(thisToken.token));
else if (!lookAheadToken().token.equals("("))
err(MyLocale.getMsg(1724, "Variable not set: ") + thisToken.token);
else {// Must be a function definition
funcDef = getFunctionDefinition(thisToken.token); // Does not return if function not defined or ambiguous
parseFunction(funcDef);
}
} else if (thisToken.tt == TokenObj.TT_NUMBER) {
calcStack.add(getNumber(thisToken.token));
} else if (thisToken.tt == TokenObj.TT_STRING) {
calcStack.add(thisToken.token);
} else if (thisToken.token.equals("(")) {
getToken();
parseStringExp();
checkNextSymIs(")");
} else
err(MyLocale.getMsg(1725, "Unexpected character(s): ") + thisToken.token);
getToken();
}
private void parseFunction(fnType funcDef) throws Exception {
String funcName = thisToken.token;
int nargs = 0;
getToken();
checkNextSymIs("(");
getToken();
if (!thisToken.token.equals(")")) { // at least one argument
parseStringExp();
nargs = 1;
while (thisToken.token.equals(",")) {
if (nargs == 4)
err(MyLocale.getMsg(1726, "Too many arguments for function ") + funcName);
getToken();
parseStringExp();
nargs++;
}
checkNextSymIs(")");
}
// getToken(); done in parseFactor
executeFunction(funcName, nargs, funcDef);
}
private void executeFunction(String funcName, int nargs, fnType funcDef) throws Exception {
if (!funcDef.nargsValid(nargs))
err(MyLocale.getMsg(1727, "Invalid number of arguments"));
if (funcDef.alias.equals("asin"))
calcStack.add(new java.lang.Double(makeDegree(java.lang.Math.asin(popCalcStackAsNumber(0)))));
else if (funcDef.alias.equals("abs"))
calcStack.add(new java.lang.Double(java.lang.Math.abs(popCalcStackAsNumber(0))));
else if (funcDef.alias.equals("acos"))
calcStack.add(new java.lang.Double(makeDegree(java.lang.Math.acos(popCalcStackAsNumber(0)))));
else if (funcDef.alias.equals("atan"))
calcStack.add(new java.lang.Double(makeDegree(java.lang.Math.atan(popCalcStackAsNumber(0)))));
else if (funcDef.alias.equals("bearing"))
calcStack.add(new java.lang.Double(funcBearing()));
else if (funcDef.alias.equals("center"))
funcCenter(nargs);
else if (funcDef.alias.equals("cls"))
funcCls();
else if (funcDef.alias.equals("cos"))
calcStack.add(new java.lang.Double(java.lang.Math.cos(makeRadiant(popCalcStackAsNumber(0)))));
else if (funcDef.alias.equals("count"))
funcCount();
else if (funcDef.alias.equals("cp"))
calcStack.add(funcCp());
else if (funcDef.alias.equals("ct"))
calcStack.add(new java.lang.Double(funcCrossTotal(nargs)));
else if (funcDef.alias.equals("deg"))
funcDeg(true);
else if (funcDef.alias.equals("deg2rad"))
calcStack.add(new java.lang.Double(funcDeg2Rad()));
else if (funcDef.alias.equals("distance"))
calcStack.add(new java.lang.Double(funcDistance()));
else if (funcDef.alias.equals("encode"))
calcStack.add(funcEncode());
else if (funcDef.alias.equals("format"))
calcStack.add(funcFormat(nargs));
else if (funcDef.alias.equals("goto"))
funcGoto(nargs);
else if (funcDef.alias.equals("ic"))
funcIgnoreVariableCase(nargs);
else if (funcDef.alias.equals("instr"))
calcStack.add(new Double(funcInstr(nargs)));
else if (funcDef.alias.equals("int"))
calcStack.add(new Double(new Double(popCalcStackAsNumber(0)).longValue()));
else if (funcDef.alias.equals("lc"))
calcStack.add(popCalcStackAsString().toLowerCase());
else if (funcDef.alias.equals("len"))
calcStack.add(new Double(popCalcStackAsString().length()));
else if (funcDef.alias.equals("mid"))
calcStack.add(funcMid(nargs));
else if (funcDef.alias.equals("mod"))
calcStack.add(funcMod());
else if (funcDef.alias.equals("project"))
calcStack.add(funcProject());
else if (funcDef.alias.equals("pz"))
funcPz(nargs);
else if (funcDef.alias.equals("rad"))
funcDeg(false);
else if (funcDef.alias.equals("rad2deg"))
calcStack.add(new java.lang.Double(funcRad2Deg()));
else if (funcDef.alias.equals("replace"))
calcStack.add(funcReplace());
else if (funcDef.alias.equals("reverse"))
calcStack.add(funcReverse(popCalcStackAsString()));
else if (funcDef.alias.equals("rot13"))
calcStack.add(Common.rot13(popCalcStackAsString()));
// else if (funcDef.alias.equals("rs")) funcRequireSemicolon(nargs);
else if (funcDef.alias.equals("show"))
;
else if (funcDef.alias.equals("sin"))
calcStack.add(new java.lang.Double(java.lang.Math.sin(makeRadiant(popCalcStackAsNumber(0)))));
else if (funcDef.alias.equals("skeleton"))
funcSkeleton(nargs);
else if (funcDef.alias.equals("sqrt"))
calcStack.add(new java.lang.Double(funcSqrt()));
else if (funcDef.alias.equals("sval"))
calcStack.add(funcSval(popCalcStackAsString()));
else if (funcDef.alias.equals("tan"))
calcStack.add(new java.lang.Double(java.lang.Math.tan(makeRadiant(popCalcStackAsNumber(0)))));
else if (funcDef.alias.equals("uc"))
calcStack.add(popCalcStackAsString().toUpperCase());
else if (funcDef.alias.equals("val"))
calcStack.add(new java.lang.Double(funcVal(popCalcStackAsString())));
else if (funcDef.alias.equals("cb"))
calcStack.add(funcCrossBearing());
else
err(MyLocale.getMsg(1728, "Function not yet implemented: ") + funcName);
}
public void parse(Vector tck, Vector msgStack) {
calcStack.clear();
clearLocalSymbols();
tokenStack = tck;
messageStack = msgStack;
scanpos = 0;
try {
parseCommand();
} catch (Exception ex) {
}
}
private boolean createWptIfNeeded(String wayPoint, String name, byte type) {
int ci = MainForm.profile.getCacheIndex(wayPoint);
if (ci >= 0)
return false;
CacheHolder ch = new CacheHolder();
ch.setCode(wayPoint);
ch.setType(type);
ch.setSize(CacheSize.CW_SIZE_NOTCHOSEN);
ch.setDifficulty(CacheTerrDiff.CW_DT_UNSET);
ch.setTerrain(CacheTerrDiff.CW_DT_UNSET);
ch.setName(name);
MainForm.profile.setAddiRef(ch);
MainForm.profile.cacheDB.add(ch);
MainTab.itself.tablePanel.myTableModel.numRows++;
return true;
}
}