Tips & Notes
Strict Mode#
JCM Scripting uses JavaScript's strict mode, which does not allow variables to be assigned without first declaring them.
This means you can't do local = 1 and expect local to be automatically defined.
Instead, you have use syntax like var glb; or outside a function var glb = 1;. For local variables inside a function, use let local; or let local = 1;.
Translator Note
In general, it is better to always try to use let and resort to using var as a last resort.
Non-standard const variable behaviour#
A variable declared using the const keyword cannot be changed throughout the execution, which improves readability and safe-guarding.
In modern JS context, it can be used like following:
Notably this will NOT work under JCM Scripting, since the const behaviour in the Rhino JS Engine is function-scoped.
This means that each function can have a const variable of the same name, but it is not block-scoped. A for-loop like above would result in re-declaration of the const variable, and an error being thrown.
Use let as an alternative if you need block-scoped variable.
Don't Block Or Infinitely Loop#
The function you wrote are called once per frame by JCM, where your function is expected to finish processing and return a value as soon/fast as possible. As such, there's no such concept as “wait for a while before continue executing”.
Instead, what you likely want is to "do a thing later on", in such case you will need to time it and then execute the appropriate action on a call made at the right time.
If you are trying to execute a long-running operation (e.g. Fetching data over the internet), you should submit it to another thread/executor.
If blocks or infinite loops did occur, then it can also affect other scripts, since some scripts will share the same thread as your script.
In the case of an infinite loop, the script execution will be aborted by JCM after ~10s of execution, and a regular runtime error will be thrown.
Avoid using internal APIs#
Any methods/fields that is not documented in this docs, as well as items within MTRClientData and VehicleWrapper.getMtrVehicle() is considered internal API.
While you can use these methods/fields, there is no guarentee these items will continue to work in future versions, which makes it less ideal for publicly-distributed pack.
If you think there's a use case which isn't covered by the API, feel free to raise an issue and see whether the use case is sensible enough and can be implemented in a backward compatible way.
JS Operators & Null Handling#
Various methods in the JCM API would return a null value (Denoted by a ? at the end of the return type), which means such value does not exist. (For various reasons, maybe the data is not fetched, or the data cannot be located)
You cannot access a property or call a method of a null value, doing so would result in an error of your script.
To cope with it, there are a couple of ways.
Traditional Null Check#
let route = getRouteOrNull(); // Dummy function, assume it returns null
if(route != null) {
let routeName = route.getName();
// Do the rest of operation
}
Optionally you can chain an else statement so you can perform a fallback operation.
let route = getRouteOrNull(); // Dummy function, assume it returns null
if(route != null) {
let routeName = route.getName();
// Do the rest of operation
} else {
// Do fallback operation
}
Conditional (ternary) operator#
Let's say routeName is only a minor part of our code. Even route is null, we still want our code to be executed and displayed.
In this case, we want to default routeName to a default/dummy value, so our code can move on.
Let's say we use Default Route as the default value if route is null. Then we can do the check like:
let route = getRouteOrNull();
let routeName;
if(route == null) {
routeName = "Default Route";
} else {
routeName = route.getName();
}
However this is cumbersome to write. As such, the conditional (ternary) operator in JS allows us to condense if else into 1 line:
Thus the above code can be represented as:
let route = getRouteOrNull();
let routeName = route == null ? "Default Route" : route.getName();
// Or an inverted condition
let routeName = route != null ? route.getName() : "Default Route";
Of course for complex conditions, it may still be better to separate it for better readability.
Optional chaining (?.)#
In the above example, route is said to return null.
Let's say we only desire the routeName like above, but at the same time we don't care about a default value, such that:
- If
routeis null, thenrouteNameis null - Otherwise,
routeNameisroute.getName()
This can be expressed using the above conditional (ternary) operator:
But this starts to get redundant (If route is null then it is null).
Thus the optional chaining operator is introduced to make us write even less code!
The above code can be represented using the optional chaining operator:
let route = getRouteOrNull();
let routeName = route?.getName(); // If route exist, then continue executing after "?.". Otherwise, return null.
Or if you only care about routeName, then it can be further condensed down:
This operator can be chained further down on nested element as well, making it a very useful operator:
let routeNameLength = getRouteOrNull()?.getName()?.length(); // Remark: We assume the returned string is a Java String, thus length() is used
if(routeNameLength != null) { // ...
Nullish coalescing operator (??)#
Finally we have the Nullish coalescing operator.
Notice how with the Optional chaining operator, the value returned would still be null?
If we want a default value (i.e. Default routeNameLength to 0), we would need to do another check:
let routeNameLength = getRouteOrNull()?.getName()?.length();
if(routeNameLength == null) routeNameLength == 0;
// ...
The nullish coalescing operator solves this issue with the following syntax:
Thus to default routeNameLength to 0, we can use the following js code:
Interoperability between Java Classes/Methods#
For common function types such as strings, Java and JavaScript have different class implementations, which causes there to be JavaScript strings as well as Java strings.
NTE/MTR functions and fields return Java string classes rather than JavaScript strings, but Rhino does some conversions automatically, so in most cases you can mix and match, but sometimes this can cause problems.
For example, here's an example of a problem caused by using str.length() from a Java string class and str.length from a JavaScript string class to get the length of a string:
var stationName = train.getThisRoutePlatforms().get(0).station.name;
print(stationName.length); // Error: stationName is a Java string, not a JavaScript string
print(stationName.length()); // Java strings get their length from the length() function, not the JavaScript length field
print((""+stationName).length) // Use ""+ to turn it into a JavaScript string.
List<T> type in Java. It does the same thing as arrays in JavaScript, but has a different type. Trying to call JavaScript array functions on it won't work, but Rhino adapts it so that you can take values with list[0] or loop through them with for (a of list).
Calling Java Class Methods#
Introducing overloading in Java#
In Java, a class can have several methods with different parameter types sharing the same identifier/name; this is known as overloading. Usually, they were designed to achieve the same result. The method to call is determined by the types of the arguments passed.
Upon calling a Java class method in JavaScript, Rhino will convert the arguments from JavaScript types into Java types according to the declaration of the preferred methods.
To be a preferred method, it must...
- Have the same number of parameters as the number of arguments given
- Have all the parameters type convertible from the arguments given
- Have a higher order of preference for its parameters compared to other methods
Order of preference may become ambiguous
The order of preference of two or more methods may level out. For example,
Since neither method is closer to the argument types than the other, Rhino will throw an error.The order of preference is specified. Since numbers are often causing problems, an order of preference of number is attached here.
Java argument type in decreasing order of preference
- double
- java.lang.Double
- float
- long
- int
- short
- char
- byte
- java.lang.String
- java.lang.Object
Sometimes, arguments may not be converted as expected. For example, a number 11 is converted into double regardless of whether a decimal place is not provided. End up referring to a different method, which may raise an error. Developers should take extra care with argument conversion.
You may refer to...
- Rhino's Tutorials #Calling Overloaded Methods for the details of Rhino's runtime overload resolution.
- Java Method Overloading and LiveConnect 3 for a more precise definition of overloading semantics.
Example#
The Java AWT class Color has the following constructors.
/*1.*/ Color(ColorSpace cspace, float[] components, float alpha)
/*2.*/ Color(float r, float g, float b)
/*3.*/ Color(float r, float g, float b, float a)
/*4.*/ Color(int rgb)
/*5.*/ Color(int rgba, boolean hasalpha)
/*6.*/ Color(int r, int g, int b)
/*7.*/ Color(int r, int g, int b, int a)
Calling Java constructors in JavaScript:
let colorFromConstructor4 = new Color(2638978); // Type signature (number) pairs with 4. (int)
let colorFromConstructor5 = new Color(0x7FFFFFFF, true); // Type signature (number, boolean) pairs with 5. (int, boolean)
let colorAutoConverted = new Color("0x80B846"); // String is converted to integer for constructor 4, as it has 1 parameter, matched the no. of arguments given
let colorFromConstructor2 = new Color(1, 1, 1); // Number is converted to float, as float has higher order of perference than intger, regardless of whether a decimal place is not provided.
let colorFromNoConstructor = new Color(); // It does not pair with any constructor
// Error! com.lx862.mtrscripting.lib.org.mozilla.javascript.EvaluatorException: Java constructor for "java.awt.Color" with arguments "" not found.
Possible misunderstanding while reading error messages
Developers should also consider of wrong arguments type when encountering any method not found errors. As instead of directly indicating that the argument types are incorrect, Rhino report that the required method with the specific type signature is not found. (...with arguments XXX not found.)
Explicit Method Specification#
From the example of calling the constructor 2 above, you may already see that, JavaScript number may not be converted to a Java type as expected. So, alternatively, Rhino/LiveConnect3 provided a way to specific the method to use, bypassing the resolution process.
To specify the method, call it as a key of an object, with the type signature suffixed.
let colorFromConstructor6 = new Color["(int,int,int)"](40, 68, 130); // No identifer for constructors
graphics["drawString(java.lang.String,int,int)"]("Overload resolution", 15, 60);
Note
- Methods/Constructors identifier are case-sensitive
- Leave the identifier blank for a constructor
- Only fully qualified name (
java.lang.String) is accepted, simple name (String) is not accepted
At least, good luck on avoiding "method not found" error! ^_^
What JavaScript features are supported?#
The Rhino JavaScript Engine does not support all of the latest JavaScript features. See Mozilla's documentation for details on what is supported.
JCM uses Rhino version 1.9.2. (NTE uses Rhino 1.7.15 with the VERSION_ES6 flag enabled.)