|
|
@@ -10,6 +10,8 @@ import org.matheclipse.core.interfaces.IAST
|
|
|
import org.matheclipse.core.interfaces.IExpr
|
|
|
import org.matheclipse.core.interfaces.INum
|
|
|
import org.matheclipse.core.interfaces.ISymbol
|
|
|
+import org.json.JSONArray
|
|
|
+import org.json.JSONObject
|
|
|
import java.util.concurrent.ConcurrentHashMap
|
|
|
import java.util.concurrent.ExecutorService
|
|
|
import java.util.concurrent.Executors
|
|
|
@@ -261,6 +263,126 @@ object SymjaBridge {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 求值一个绘图表达式,并把 Symja 返回的 `Graphics` **表达式树**转成 JSON。
|
|
|
+ *
|
|
|
+ * 返回形如:
|
|
|
+ * `{"type":"plot","elements":[{"type":"line","color":"#5E81B5","points":[[x,y],...]}],
|
|
|
+ * "options":{"Axes":"True", ...}}`
|
|
|
+ *
|
|
|
+ * 刻意**不移植** Ijs 那套 829 行的 `parseGraphicsToJson` —— 它是扫描 `Graphics(...)`
|
|
|
+ * 文本,脆且难维护。这里直接走表达式树:
|
|
|
+ * `Graphics( {RGBColor[...], Line[{{x,y},...]}, Point[{...}], ...}, Rule[...], ... )`
|
|
|
+ * 遍历时记住最近一个 RGBColor,作为其后图元的颜色。
|
|
|
+ */
|
|
|
+ fun plotSync(expr: String, timeoutMs: Long): String {
|
|
|
+ val f = worker.submit<String> {
|
|
|
+ try {
|
|
|
+ val r = evaluator.evaluateWithTimeout(
|
|
|
+ expr, timeoutMs, TimeUnit.MILLISECONDS, true, null
|
|
|
+ )
|
|
|
+ if (r == null) {
|
|
|
+ "Error: 绘图超时(${timeoutMs}ms)或被取消"
|
|
|
+ } else {
|
|
|
+ graphicsToJson(r) ?: r.toString() // 不是 Graphics 就原样返回
|
|
|
+ }
|
|
|
+ } catch (t: Throwable) {
|
|
|
+ "Error: " + (failure(t)["error"] ?: t.toString())
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return try {
|
|
|
+ f.get(timeoutMs + 5_000, TimeUnit.MILLISECONDS)
|
|
|
+ } catch (t: Throwable) {
|
|
|
+ f.cancel(true)
|
|
|
+ "Error: " + t.javaClass.simpleName
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 把 `Graphics(...)` 表达式树转成 JSON;不是 Graphics 就返回 null。 */
|
|
|
+ private fun graphicsToJson(g: IExpr): String? {
|
|
|
+ if (g !is IAST || !g.head().toString().equals("Graphics", ignoreCase = true)) return null
|
|
|
+ if (g.size() < 2) return null
|
|
|
+ val primitives = g.get(1)
|
|
|
+ if (primitives !is IAST) return null
|
|
|
+
|
|
|
+ var color: String? = null
|
|
|
+ val elements = JSONArray()
|
|
|
+ for (p in primitives) {
|
|
|
+ if (p !is IAST) continue
|
|
|
+ when (p.head().toString()) {
|
|
|
+ "RGBColor" -> color = rgbToHex(p)
|
|
|
+
|
|
|
+ "Line" -> pointsOf(p)?.let {
|
|
|
+ elements.put(
|
|
|
+ JSONObject()
|
|
|
+ .put("type", "line")
|
|
|
+ .put("color", color ?: "#000000")
|
|
|
+ .put("points", it)
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ "Point" -> pointsOf(p)?.let {
|
|
|
+ elements.put(
|
|
|
+ JSONObject()
|
|
|
+ .put("type", "point")
|
|
|
+ .put("color", color ?: "#000000")
|
|
|
+ .put("points", it)
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Graphics 的第 2 个参数起是 Rule 形式的选项(Axes->True 之类)
|
|
|
+ val options = JSONObject()
|
|
|
+ for (i in 2 until g.size()) {
|
|
|
+ val rule = g.get(i)
|
|
|
+ if (rule is IAST && rule.head().toString() == "Rule" && rule.size() >= 3) {
|
|
|
+ options.put(rule.get(1).toString(), rule.get(2).toString())
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return JSONObject()
|
|
|
+ .put("type", "plot")
|
|
|
+ .put("elements", elements)
|
|
|
+ .put("options", options)
|
|
|
+ .toString()
|
|
|
+ }
|
|
|
+
|
|
|
+ /** `Line[{{x,y},...}]` / `Point[{...}]` 里的坐标列表。 */
|
|
|
+ private fun pointsOf(prim: IAST): JSONArray? {
|
|
|
+ if (prim.size() < 2) return null
|
|
|
+ val list = prim.get(1)
|
|
|
+ if (list !is IAST) return null
|
|
|
+ val out = JSONArray()
|
|
|
+ for (pt in list) {
|
|
|
+ if (pt !is IAST || pt.size() < 3) continue
|
|
|
+ val x = pt.get(1).asDouble() ?: continue
|
|
|
+ val y = pt.get(2).asDouble() ?: continue
|
|
|
+ out.put(JSONArray().put(x).put(y))
|
|
|
+ }
|
|
|
+ return if (out.length() == 0) null else out
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun rgbToHex(c: IAST): String? {
|
|
|
+ if (c.size() < 4) return null
|
|
|
+ val r = c.get(1).asDouble() ?: return null
|
|
|
+ val g = c.get(2).asDouble() ?: return null
|
|
|
+ val b = c.get(3).asDouble() ?: return null
|
|
|
+ return String.format(
|
|
|
+ "#%02X%02X%02X",
|
|
|
+ (r * 255).toInt().coerceIn(0, 255),
|
|
|
+ (g * 255).toInt().coerceIn(0, 255),
|
|
|
+ (b * 255).toInt().coerceIn(0, 255),
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 数值表达式转 double;非数值(ComplexInfinity、符号等)返回 null。 */
|
|
|
+ private fun IExpr.asDouble(): Double? = try {
|
|
|
+ if (this is INum) doubleValue() else toString().toDoubleOrNull()
|
|
|
+ } catch (t: Throwable) {
|
|
|
+ null
|
|
|
+ }
|
|
|
+
|
|
|
/** 同步取当前全局变量的 `名字 = 值` 文本,一行一个。 */
|
|
|
fun variablesSync(): String {
|
|
|
val f = worker.submit<String> {
|