poi-plugin-compass 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/compass.css +17 -2
- package/data/maps.json +7 -13
- package/data/normal-maps.json +168 -154
- package/docs/research.md +7 -5
- package/docs/sources.md +1 -1
- package/index.js +83 -14
- package/logic.js +164 -15
- package/package.json +2 -2
package/docs/research.md
CHANGED
|
@@ -131,7 +131,7 @@ const label = maps?.[mapKey]?.route?.[Number(cell)]?.[1]
|
|
|
131
131
|
|
|
132
132
|
无法确定“约 80%”是否为长期规则、样本估计还是攻略近似时,应使用 `approximate` 或 `unknown`,并把原文保留到审阅队列。
|
|
133
133
|
|
|
134
|
-
本轮已将通常海域 36 张 `/{带路条件}` 子页以及 5-6 主页面的原始 wikitext 缓存到 `data/sources/kcwiki/routes/`,并在 `manifest.json` 中记录 revision ID、修订时间和 SHA-256。规则校正以这批缓存为审阅入口:`CV系`/`CV*` 归入 `CV_ALL`,明确的 `CV+CVB` 归入 `CV_MAIN`;`CL系`、`CL+CT`、`BB系` 和单独 `BB`
|
|
134
|
+
本轮已将通常海域 36 张 `/{带路条件}` 子页以及 5-6 主页面的原始 wikitext 缓存到 `data/sources/kcwiki/routes/`,并在 `manifest.json` 中记录 revision ID、修订时间和 SHA-256。规则校正以这批缓存为审阅入口:`CA系`/`CA+CAV` 归入 `CA_ALL`,`CV系`/`CV*` 归入 `CV_ALL`,明确的 `CV+CVB` 归入 `CV_MAIN`;`CL系`、`CL+CT`、`BB系` 和单独 `BB` 也分别使用不同令牌。`CV_ALL` 是 CV+CVB+CVL,`CV_MAIN` 只含 CV+CVB;`CL_ALL` 是 CL+CLT+CT;`BB_ALL` 是 BB+FBB+BBV,而单独 `BB` 只含 BB+FBB,避免把 CVL、CT、BBV 或 CAV 静默漏掉或误计入。
|
|
135
135
|
|
|
136
136
|
### 4.2 日文 Wiki
|
|
137
137
|
|
|
@@ -247,6 +247,8 @@ interface RouteRule {
|
|
|
247
247
|
to: NodeLabel
|
|
248
248
|
probability: number | 'unknown'
|
|
249
249
|
}>
|
|
250
|
+
// 原文明确表示本次概率判定失败后继续检查后续规则
|
|
251
|
+
continueOnFailure?: boolean
|
|
250
252
|
appliesWhen?: { difficulty?: string; gauge?: number; phase?: number }
|
|
251
253
|
sourceRefs: SourceRef[]
|
|
252
254
|
confidence: 'exact' | 'verified' | 'parsed' | 'approximate' | 'unknown'
|
|
@@ -287,11 +289,11 @@ interface EnemyComposition {
|
|
|
287
289
|
|
|
288
290
|
1. 按 `priority` 排序;
|
|
289
291
|
2. 依次求值规则的 predicate;
|
|
290
|
-
3.
|
|
291
|
-
4.
|
|
292
|
-
5.
|
|
292
|
+
3. 对命中的规则按其出口概率消费当前剩余概率质量;
|
|
293
|
+
4. 若规则标记 `continueOnFailure`,或已知出口概率之和小于 1,则只消费已知命中概率,把失败的剩余质量交给同节点后续规则;
|
|
294
|
+
5. 没有后续规则时,才将明确的未知随机出口均分并标记估算;predicate 本身无法判断时返回 `unknown`,不越过该条件猜测后续规则。
|
|
293
295
|
|
|
294
|
-
|
|
296
|
+
因此页面中的“第一条规则”只是在没有续判语义时消耗全部质量;`4-3 H`、`2-3 J`、`4-5 Q` 等节点会把多个命中规则组合成复合概率,而不是把前一条的失败质量强行并入另一个出口。
|
|
295
297
|
|
|
296
298
|
### 6.2 概率质量传播
|
|
297
299
|
|
package/docs/sources.md
CHANGED
|
@@ -82,4 +82,4 @@ data/sources/kcwiki/routes/manifest.json
|
|
|
82
82
|
|
|
83
83
|
`1-1`~`1-6`、`2-1`~`2-5`、`3-1`~`3-5`、`4-1`~`4-5`、`5-1`~`5-6`、`6-1`~`6-5`、`7-1`~`7-5`。5-6 的路线分支直接记录在海域主页面 `南方海域/5-6`,因此缓存脚本对该图使用主页面标题,而其他通常图继续使用 `/{带路条件}` 子页。
|
|
84
84
|
|
|
85
|
-
其中带有“约”“随机但未给比例”“原文带问号”或依赖分歧点系数/司令部系数的条件,使用 `approximate` 或 `unknown
|
|
85
|
+
其中带有“约”“随机但未给比例”“原文带问号”或依赖分歧点系数/司令部系数的条件,使用 `approximate` 或 `unknown`;这类记录仍提供可能出口,但不产生伪造的全局百分比。原文明确写出“判定失败后继续”“继续下面判定”或“逐条依次判定”时,规则会保留已知命中概率并设置 `continueOnFailure`,将剩余概率交给同节点后续规则。当前临时估计约定为:未给出比例的两个出口按 50%/50% 并标记 `?`;索敌区间内“概率随索敌变化”的两个出口也按 50%/50% 并标记 `?`;只有一个出口且明确表示失败后继续判定时,该出口按 50% 并把剩余 50% 交给后续规则。固定单出口仍按 100% 处理,但保留未知/近似标记。规则面板对 KCWiki 原文中的多条同向条件逐行显示,评估器仍按来源顺序处理。7-3 A 点曾将去 B 和去 C 的条件错误合并,已按缓存原文顺序拆开并修正出口。后续继续补录时,先以同一 API 子页取得规则原文,再用日文 Wiki 或浏览器中的 NGA 页面复核有冲突的条件。
|
package/index.js
CHANGED
|
@@ -404,6 +404,58 @@ function outcomeText(outcome) {
|
|
|
404
404
|
return `${outcome.to} ${formatPercent(outcome.probability, outcome.estimated)}`
|
|
405
405
|
}
|
|
406
406
|
|
|
407
|
+
// These predicates came from separate list items in the cached KCWiki route
|
|
408
|
+
// tables. Keep the evaluator's compact OR semantics for compatibility, but
|
|
409
|
+
// render each source line separately so the UI does not invent one combined
|
|
410
|
+
// rule where the source did not have one.
|
|
411
|
+
const SEPARATE_SOURCE_LINE_RULE_IDS = new Set([
|
|
412
|
+
'2-5-C-heavy',
|
|
413
|
+
'1-5-F-heavy',
|
|
414
|
+
'1-6-start-heavy', '1-6-M-heavy',
|
|
415
|
+
'2-3-D-special',
|
|
416
|
+
'4-2-C-light-l',
|
|
417
|
+
'6-1-start-heavy3',
|
|
418
|
+
'6-2-C-a', '6-2-D-f', '6-2-E-f',
|
|
419
|
+
'6-4-start-right-lha', '6-4-A-noakitsu-heavy', '6-4-E-special', '6-4-E-g', '6-4-J-l', '6-4-J-i', '6-4-K-h',
|
|
420
|
+
'6-5-start-left', '6-5-C-e', '6-5-E-i', '6-5-I-h',
|
|
421
|
+
'7-1-B-a', '7-1-H-k',
|
|
422
|
+
'7-2-C-d', '7-2-E-g', '7-2-I-j',
|
|
423
|
+
'7-3-C-heavy', '7-3-G-special', '7-3-I-heavy', '7-3-J-m', '7-3-J-p', '7-3-M-n', '7-3-M-o',
|
|
424
|
+
'7-4-start-c', '7-4-start-a', '7-4-C-d', '7-4-J-k', '7-4-J-l', '7-4-M-heavy',
|
|
425
|
+
'7-5-B-c', '7-5-D-e', '7-5-D-fast', '7-5-J-N', '7-5-J-O-heavy', '7-5-P-r',
|
|
426
|
+
])
|
|
427
|
+
|
|
428
|
+
function visibleRuleLabel(rule) {
|
|
429
|
+
const label = predicateLabel(rule?.predicate)
|
|
430
|
+
return label
|
|
431
|
+
.replace(/([^)]*(?:近似|约|原文|比例未知|分歧系数)[^)]*)/g, '')
|
|
432
|
+
.replace(/\([^)]*(?:approx|about|original|unknown|coefficient)[^)]*\)/gi, '')
|
|
433
|
+
.replace(/,?\s*失败后继续判定/g, '')
|
|
434
|
+
.replace(/:\s*[^,;:]+概率(?:未知|随索敌变化)/g, '')
|
|
435
|
+
.replace(/;\s*(?:近似|约)/g, '')
|
|
436
|
+
.replace(/(原文(?:标记\s*\?|比例未知)[^)]*)/g, '')
|
|
437
|
+
.replace(/(约)/g, '')
|
|
438
|
+
.replace(/\s{2,}/g, ' ')
|
|
439
|
+
.replace(/:\s*$/g, '')
|
|
440
|
+
.trim()
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function rulePopoverNotes(rule, rawLabel, visibleLabel) {
|
|
444
|
+
const notes = []
|
|
445
|
+
if (rawLabel !== visibleLabel) notes.push(`原文补充:${rawLabel}`)
|
|
446
|
+
if (rule?.continueOnFailure === true) notes.push('本条判定失败后继续检查后续规则')
|
|
447
|
+
if (rule?.confidence === 'approximate') notes.push('该条件或概率来自近似资料')
|
|
448
|
+
if (rule?.confidence === 'unknown' || rule?.outcomes?.some((outcome) => outcome?.probability == null)) {
|
|
449
|
+
notes.push('来源没有给出明确概率;当前按均等概率暂估')
|
|
450
|
+
}
|
|
451
|
+
return notes
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function displayPredicatesForRule(rule) {
|
|
455
|
+
if (!SEPARATE_SOURCE_LINE_RULE_IDS.has(rule?.id) || rule?.predicate?.kind !== 'any') return [rule?.predicate]
|
|
456
|
+
return rule.predicate.predicates?.length ? rule.predicate.predicates : [rule.predicate]
|
|
457
|
+
}
|
|
458
|
+
|
|
407
459
|
function conditionIcon(status, active) {
|
|
408
460
|
if (active) return '✓'
|
|
409
461
|
if (status === 'true') return '•'
|
|
@@ -423,11 +475,16 @@ function ruleRows(mapDefinition, node, context) {
|
|
|
423
475
|
return (mapDefinition.rules || [])
|
|
424
476
|
.filter((rule) => rule.node === node)
|
|
425
477
|
.sort((left, right) => number(left.priority) - number(right.priority))
|
|
426
|
-
.
|
|
427
|
-
rule,
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
478
|
+
.reduce((rows, rule) => {
|
|
479
|
+
displayPredicatesForRule(rule).forEach((predicate, index) => {
|
|
480
|
+
rows.push({
|
|
481
|
+
rule: { ...rule, id: `${rule.id}::${index}`, predicate, sourceRuleId: rule.id },
|
|
482
|
+
result: evaluatePredicate(predicate, context),
|
|
483
|
+
outcomes: normalizeOutcomes(rule.outcomes, rule),
|
|
484
|
+
})
|
|
485
|
+
})
|
|
486
|
+
return rows
|
|
487
|
+
}, [])
|
|
431
488
|
}
|
|
432
489
|
|
|
433
490
|
function fleetSummary(context) {
|
|
@@ -613,10 +670,10 @@ class Compass extends React.Component {
|
|
|
613
670
|
`制空 ${air.min === air.max ? air.min : `${air.min}~${air.max}`}`)
|
|
614
671
|
}
|
|
615
672
|
|
|
616
|
-
setManualOverride(node, to, edgeId = null) {
|
|
673
|
+
setManualOverride(node, to, edgeId = null, selectedNode = node) {
|
|
617
674
|
this.setState((state) => ({
|
|
618
675
|
overrides: { ...state.overrides, [node]: to },
|
|
619
|
-
selectedNode
|
|
676
|
+
selectedNode,
|
|
620
677
|
selectedEdge: edgeId,
|
|
621
678
|
exportRoute: null,
|
|
622
679
|
exportError: null,
|
|
@@ -747,7 +804,7 @@ class Compass extends React.Component {
|
|
|
747
804
|
const activateNode = (event) => {
|
|
748
805
|
if (event.key !== 'Enter' && event.key !== ' ') return
|
|
749
806
|
event.preventDefault()
|
|
750
|
-
if (manualTarget) this.setManualOverride(manualSource, node)
|
|
807
|
+
if (manualTarget) this.setManualOverride(manualSource, node, null, node)
|
|
751
808
|
else this.selectNode(node)
|
|
752
809
|
}
|
|
753
810
|
return h('g', {
|
|
@@ -755,7 +812,7 @@ class Compass extends React.Component {
|
|
|
755
812
|
className: classNames,
|
|
756
813
|
role: 'button',
|
|
757
814
|
tabIndex: 0,
|
|
758
|
-
onClick: () => manualTarget ? this.setManualOverride(manualSource, node) : this.selectNode(node),
|
|
815
|
+
onClick: () => manualTarget ? this.setManualOverride(manualSource, node, null, node) : this.selectNode(node),
|
|
759
816
|
onKeyDown: activateNode,
|
|
760
817
|
},
|
|
761
818
|
h('title', null, nodeInfo?.label ? `${node} · ${nodeInfo.label}` : node),
|
|
@@ -960,22 +1017,31 @@ class Compass extends React.Component {
|
|
|
960
1017
|
h('h4', null, `${node} 点`),
|
|
961
1018
|
rows.length
|
|
962
1019
|
? h('div', { className: 'compass-conditions' }, ...rows.map(({ rule, result, outcomes: ruleOutcomes }) => {
|
|
963
|
-
const
|
|
964
|
-
const
|
|
1020
|
+
const sourceRuleId = rule.sourceRuleId || rule.id
|
|
1021
|
+
const sourceWasMatched = decision?.matchedRuleIds?.includes(sourceRuleId) || decision?.rule?.id === sourceRuleId
|
|
1022
|
+
const splitSourceRule = SEPARATE_SOURCE_LINE_RULE_IDS.has(sourceRuleId)
|
|
1023
|
+
const active = splitSourceRule ? sourceWasMatched && result.status === 'true' : sourceWasMatched
|
|
1024
|
+
const displayedOutcomes = active && (decision?.matchedRuleIds?.length || 0) <= 1
|
|
1025
|
+
? decision.outcomes
|
|
1026
|
+
: ruleOutcomes
|
|
965
1027
|
const outcomes = displayedOutcomes.length ? displayedOutcomes.map(outcomeText).join(' / ') : '—'
|
|
1028
|
+
const rawLabel = predicateLabel(rule.predicate)
|
|
1029
|
+
const label = visibleRuleLabel(rule)
|
|
1030
|
+
const notes = rulePopoverNotes(rule, rawLabel, label)
|
|
966
1031
|
return h('div', {
|
|
967
1032
|
key: rule.id,
|
|
968
1033
|
className: `compass-condition is-${result.status}${active ? ' is-active' : ''}`,
|
|
969
1034
|
tabIndex: 0,
|
|
970
|
-
'aria-label': `${conditionStatus(result.status)}:${
|
|
1035
|
+
'aria-label': `${conditionStatus(result.status)}:${label}`,
|
|
971
1036
|
},
|
|
972
1037
|
h('span', { className: 'compass-condition-icon', 'aria-hidden': 'true' }, conditionIcon(result.status, active)),
|
|
973
|
-
h('span', { className: 'compass-condition-label' },
|
|
1038
|
+
h('span', { className: 'compass-condition-label' }, label),
|
|
974
1039
|
h('span', { className: 'compass-condition-outcomes' }, outcomes),
|
|
975
1040
|
h('div', { className: 'compass-condition-popover', role: 'tooltip' },
|
|
976
1041
|
h('div', null, `${conditionStatus(result.status)}${active ? ' · 当前出口规则' : ''}`),
|
|
1042
|
+
h('div', null, `条件:${rawLabel}`),
|
|
977
1043
|
h('div', null, `出口:${outcomes}`),
|
|
978
|
-
|
|
1044
|
+
...notes.map((note, index) => h('div', { key: `${rule.id}-note-${index}`, className: 'compass-condition-note' }, note)),
|
|
979
1045
|
),
|
|
980
1046
|
)
|
|
981
1047
|
}))
|
|
@@ -1100,4 +1166,7 @@ exports.__test = {
|
|
|
1100
1166
|
phaseOptionsForMap,
|
|
1101
1167
|
pluginState,
|
|
1102
1168
|
routeEdges,
|
|
1169
|
+
ruleRows,
|
|
1170
|
+
visibleRuleLabel,
|
|
1171
|
+
separateSourceLineRuleIds: SEPARATE_SOURCE_LINE_RULE_IDS,
|
|
1103
1172
|
}
|
package/logic.js
CHANGED
|
@@ -9,19 +9,20 @@ const SHIP_CATEGORIES = {
|
|
|
9
9
|
CL_ALL: new Set([3, 4, 21]),
|
|
10
10
|
CA: new Set([5]),
|
|
11
11
|
CAV: new Set([6]),
|
|
12
|
+
CA_ALL: new Set([5, 6]),
|
|
12
13
|
CVL: new Set([7]),
|
|
13
14
|
BB: new Set([8, 9]),
|
|
14
15
|
BBV: new Set([10]),
|
|
15
16
|
BB_ALL: new Set([8, 9, 10]),
|
|
16
|
-
CV: new Set([11, 12
|
|
17
|
-
CV_ALL: new Set([7, 11, 12
|
|
17
|
+
CV: new Set([11, 12]),
|
|
18
|
+
CV_ALL: new Set([7, 11, 12]),
|
|
18
19
|
SS: new Set([13, 14]),
|
|
19
20
|
AO: new Set([15]),
|
|
20
21
|
AV: new Set([16]),
|
|
21
22
|
LHA: new Set([17]),
|
|
22
23
|
AR: new Set([19]),
|
|
23
24
|
AS: new Set([20]),
|
|
24
|
-
CV_MAIN: new Set([11, 12
|
|
25
|
+
CV_MAIN: new Set([11, 12]),
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
const CATEGORY_LABELS = {
|
|
@@ -33,6 +34,7 @@ const CATEGORY_LABELS = {
|
|
|
33
34
|
CL_ALL: 'CL系',
|
|
34
35
|
CA: '重巡',
|
|
35
36
|
CAV: '航巡',
|
|
37
|
+
CA_ALL: 'CA系',
|
|
36
38
|
CVL: '轻空母',
|
|
37
39
|
BB: '战舰(BB/FBB)',
|
|
38
40
|
BBV: '航战',
|
|
@@ -45,7 +47,7 @@ const CATEGORY_LABELS = {
|
|
|
45
47
|
LHA: '扬陆',
|
|
46
48
|
AR: '工作舰',
|
|
47
49
|
AS: '潜母',
|
|
48
|
-
CV_MAIN: '
|
|
50
|
+
CV_MAIN: 'CV/CVB',
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
const EQUIPMENT_CATEGORY_LABELS = {
|
|
@@ -313,17 +315,152 @@ function normalizeOutcomes(outcomes, rule = null, options = {}) {
|
|
|
313
315
|
: Number.isFinite(Number(outcome.probability)) ? Number(outcome.probability) : null,
|
|
314
316
|
}))
|
|
315
317
|
|
|
318
|
+
if (!normalized.length) return []
|
|
319
|
+
const hasKnownProbability = normalized.some((outcome) => outcome.probability != null)
|
|
316
320
|
const hasMissingProbability = normalized.some((outcome) => outcome.probability == null)
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
321
|
+
const allUnknown = !hasKnownProbability
|
|
322
|
+
if (rule?.continueOnFailure === true) {
|
|
323
|
+
if (!hasKnownProbability) {
|
|
324
|
+
// A rule such as “within this LOS range, go to N; if the check fails,
|
|
325
|
+
// continue below” has two effective branches. The source does not give
|
|
326
|
+
// a ratio, so expose the same temporary 50/50 estimate used by the
|
|
327
|
+
// route evaluator instead of showing an unhelpful “unknown” row.
|
|
328
|
+
const probability = 1 / (normalized.length + 1)
|
|
329
|
+
return normalized.map((outcome) => ({ to: outcome.to, probability, estimated: true }))
|
|
330
|
+
}
|
|
331
|
+
const estimated = rule.confidence === 'unknown' || rule.confidence === 'approximate'
|
|
332
|
+
return normalized
|
|
333
|
+
.filter((outcome) => outcome.probability != null)
|
|
334
|
+
.map((outcome) => ({
|
|
335
|
+
to: outcome.to,
|
|
336
|
+
probability: outcome.probability,
|
|
337
|
+
...(estimated ? { estimated: true } : {}),
|
|
338
|
+
}))
|
|
339
|
+
}
|
|
340
|
+
if (allUnknown && options.equalizeUnknown === false) {
|
|
341
|
+
return normalized.map((outcome) => ({ to: outcome.to, probability: null }))
|
|
342
|
+
}
|
|
343
|
+
const equalize = options.equalizeUnknown !== false && (
|
|
344
|
+
allUnknown || !hasMissingProbability && rule?.confidence === 'unknown'
|
|
345
|
+
)
|
|
346
|
+
const estimated = equalize || hasMissingProbability || rule?.confidence === 'unknown'
|
|
347
|
+
|| rule?.confidence === 'approximate' && normalized.length > 1
|
|
348
|
+
return normalized
|
|
349
|
+
.filter((outcome) => equalize || outcome.probability != null)
|
|
350
|
+
.map((outcome) => {
|
|
320
351
|
const normalizedOutcome = {
|
|
321
352
|
to: outcome.to,
|
|
322
353
|
probability: equalize && normalized.length ? 1 / normalized.length : outcome.probability,
|
|
323
354
|
}
|
|
324
355
|
if (estimated) normalizedOutcome.estimated = true
|
|
325
356
|
return normalizedOutcome
|
|
326
|
-
|
|
357
|
+
})
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function ruleDistribution(rule) {
|
|
361
|
+
const raw = (Array.isArray(rule?.outcomes) ? rule.outcomes : [])
|
|
362
|
+
.filter((outcome) => outcome && typeof outcome.to === 'string')
|
|
363
|
+
.map((outcome) => ({
|
|
364
|
+
to: outcome.to,
|
|
365
|
+
probability: outcome.probability == null
|
|
366
|
+
? null
|
|
367
|
+
: Number.isFinite(Number(outcome.probability)) ? Number(outcome.probability) : null,
|
|
368
|
+
}))
|
|
369
|
+
if (!raw.length) return { outcomes: [], residual: rule?.continueOnFailure === true ? 1 : 0 }
|
|
370
|
+
|
|
371
|
+
const known = raw.filter((outcome) => outcome.probability != null)
|
|
372
|
+
const knownTotal = known.reduce((total, outcome) => total + outcome.probability, 0)
|
|
373
|
+
const partial = rule?.continueOnFailure === true
|
|
374
|
+
|| (known.length > 0 && knownTotal < 1 - 1e-9)
|
|
375
|
+
|| (known.length > 0 && known.length < raw.length)
|
|
376
|
+
|
|
377
|
+
if (partial) {
|
|
378
|
+
if (known.length) {
|
|
379
|
+
return {
|
|
380
|
+
outcomes: normalizeOutcomes(raw, rule),
|
|
381
|
+
residual: Math.max(0, 1 - knownTotal),
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const probability = 1 / (raw.length + 1)
|
|
385
|
+
return {
|
|
386
|
+
outcomes: raw.map((outcome) => ({ to: outcome.to, probability, estimated: true })),
|
|
387
|
+
residual: probability,
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return { outcomes: normalizeOutcomes(raw, rule), residual: 0 }
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function evaluateRouteRules(mapDefinition, node, context) {
|
|
395
|
+
const rules = rulesForNode(mapDefinition, node)
|
|
396
|
+
if (!rules.length) {
|
|
397
|
+
return { status: 'terminal', rule: null, predicate: null, matchedRuleIds: [], outcomes: [], unknownProbability: 0 }
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const totals = new Map()
|
|
401
|
+
const matchedRuleIds = []
|
|
402
|
+
let residual = 1
|
|
403
|
+
let firstRule = null
|
|
404
|
+
let firstPredicate = null
|
|
405
|
+
let unresolvedPredicate = null
|
|
406
|
+
let terminal = false
|
|
407
|
+
|
|
408
|
+
function addOutcome(outcome, mass) {
|
|
409
|
+
if (!Number.isFinite(mass) || mass <= 0) return
|
|
410
|
+
const current = totals.get(outcome.to)
|
|
411
|
+
if (!current) {
|
|
412
|
+
totals.set(outcome.to, {
|
|
413
|
+
to: outcome.to,
|
|
414
|
+
probability: mass,
|
|
415
|
+
...(outcome.estimated === true ? { estimated: true } : {}),
|
|
416
|
+
})
|
|
417
|
+
return
|
|
418
|
+
}
|
|
419
|
+
current.probability += mass
|
|
420
|
+
if (outcome.estimated === true) current.estimated = true
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
for (const rule of rules) {
|
|
424
|
+
if (residual <= 1e-9) break
|
|
425
|
+
const predicate = evaluatePredicate(rule.predicate, context)
|
|
426
|
+
if (predicate.status === 'false') continue
|
|
427
|
+
if (predicate.status === 'unknown') {
|
|
428
|
+
unresolvedPredicate = { rule, predicate }
|
|
429
|
+
break
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (!firstRule) {
|
|
433
|
+
firstRule = rule
|
|
434
|
+
firstPredicate = predicate
|
|
435
|
+
}
|
|
436
|
+
if (rule.id) matchedRuleIds.push(rule.id)
|
|
437
|
+
if (rule.terminal === true) {
|
|
438
|
+
residual = 0
|
|
439
|
+
terminal = true
|
|
440
|
+
break
|
|
441
|
+
}
|
|
442
|
+
const distribution = ruleDistribution(rule)
|
|
443
|
+
if (!distribution.outcomes.length) {
|
|
444
|
+
unresolvedPredicate = { rule, predicate }
|
|
445
|
+
break
|
|
446
|
+
}
|
|
447
|
+
distribution.outcomes.forEach((outcome) => addOutcome(outcome, residual * outcome.probability))
|
|
448
|
+
residual *= distribution.residual
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const outcomes = [...totals.values()]
|
|
452
|
+
const unknownProbability = (unresolvedPredicate || residual > 1e-9) ? residual : 0
|
|
453
|
+
const status = unknownProbability > 1e-9
|
|
454
|
+
? 'unknown'
|
|
455
|
+
: outcomes.length ? 'matched' : terminal ? 'terminal' : 'unknown'
|
|
456
|
+
return {
|
|
457
|
+
status,
|
|
458
|
+
rule: firstRule || unresolvedPredicate?.rule || null,
|
|
459
|
+
predicate: firstPredicate || unresolvedPredicate?.predicate || null,
|
|
460
|
+
matchedRuleIds,
|
|
461
|
+
outcomes,
|
|
462
|
+
unknownProbability,
|
|
463
|
+
}
|
|
327
464
|
}
|
|
328
465
|
|
|
329
466
|
function routeDecision(mapDefinition, node, context, overrides = {}) {
|
|
@@ -334,9 +471,17 @@ function routeDecision(mapDefinition, node, context, overrides = {}) {
|
|
|
334
471
|
const manualOutcomes = Array.isArray(configuredManualOutcomes)
|
|
335
472
|
? normalizeOutcomes(configuredManualOutcomes, { confidence: 'unknown' }, { equalizeUnknown: false })
|
|
336
473
|
: null
|
|
337
|
-
const
|
|
338
|
-
?
|
|
339
|
-
|
|
474
|
+
const automatic = manual
|
|
475
|
+
? {
|
|
476
|
+
status: selected.status,
|
|
477
|
+
rule: selected.rule,
|
|
478
|
+
predicate: selected.predicate,
|
|
479
|
+
matchedRuleIds: selected.rule?.id ? [selected.rule.id] : [],
|
|
480
|
+
outcomes: selected.status === 'matched' ? normalizeOutcomes(selected.rule?.outcomes, selected.rule) : [],
|
|
481
|
+
unknownProbability: 0,
|
|
482
|
+
}
|
|
483
|
+
: evaluateRouteRules(mapDefinition, node, context)
|
|
484
|
+
const autoOutcomes = automatic.outcomes
|
|
340
485
|
const phaseRuleMatched = manual && hasPhaseRules(mapDefinition, node) && selected.status === 'matched'
|
|
341
486
|
const baseOutcomes = phaseRuleMatched
|
|
342
487
|
? autoOutcomes
|
|
@@ -349,9 +494,11 @@ function routeDecision(mapDefinition, node, context, overrides = {}) {
|
|
|
349
494
|
: baseOutcomes
|
|
350
495
|
return {
|
|
351
496
|
node,
|
|
352
|
-
status:
|
|
353
|
-
rule:
|
|
354
|
-
predicate:
|
|
497
|
+
status: automatic.status,
|
|
498
|
+
rule: automatic.rule,
|
|
499
|
+
predicate: automatic.predicate,
|
|
500
|
+
matchedRuleIds: automatic.matchedRuleIds,
|
|
501
|
+
unknownProbability: automatic.unknownProbability,
|
|
355
502
|
baseOutcomes,
|
|
356
503
|
manualOutcomes,
|
|
357
504
|
outcomes,
|
|
@@ -383,10 +530,12 @@ function propagateProbability(start, decisions) {
|
|
|
383
530
|
return
|
|
384
531
|
}
|
|
385
532
|
const decision = decisions[node]
|
|
386
|
-
if (!decision || decision.status === '
|
|
533
|
+
if (!decision || decision.status === 'terminal' && !decision.outcomes.length
|
|
534
|
+
|| decision.status === 'unknown' && !decision.outcomes.length) {
|
|
387
535
|
if (decision?.status === 'unknown') unknownNodes.add(node)
|
|
388
536
|
return
|
|
389
537
|
}
|
|
538
|
+
if (decision.status === 'unknown' || Number(decision.unknownProbability) > 1e-9) unknownNodes.add(node)
|
|
390
539
|
const nextPath = new Set(path)
|
|
391
540
|
nextPath.add(node)
|
|
392
541
|
decision.outcomes.forEach((outcome) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "poi-plugin-compass",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "通常海域舰队罗盘",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"repository": {
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"icon": "fa/compass",
|
|
53
53
|
"priority": 20,
|
|
54
54
|
"apiVer": {
|
|
55
|
-
"11.99.99": "0.1.
|
|
55
|
+
"11.99.99": "0.1.3"
|
|
56
56
|
}
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|