@jetta/blocks-allocator 0.2.1-0 → 0.2.1-2
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/package.json +2 -1
- package/src/Allocation_Attempt.js +4 -1
- package/src/Allocation_Optimization/Block_Allocator.js +12 -6
- package/src/Allocation_Optimization/block_builder/Block_Wall_Builder.js +8 -1
- package/src/Allocation_Optimization/block_builder/Blocks_Builder.js +3 -1
- package/src/Allocation_Optimization/block_builder/Slot_Item_Assigner.js +4 -2
- package/src/Allocation_Optimization/item_consumption_heuristic/Item_Chooser.js +6 -130
- package/src/Allocation_Optimization/item_consumption_heuristic/Slots_Items_Assigner.js +59 -32
- package/src/Allocation_Optimization/item_consumption_heuristic/assign_one_plan_slot.js +53 -0
- package/src/Allocation_Optimization/item_consumption_heuristic/axle_load/assert_layout_fits_axle_load.js +26 -0
- package/src/Allocation_Optimization/item_consumption_heuristic/axle_load/axle_load_exceeds_limit.js +62 -0
- package/src/Allocation_Optimization/item_consumption_heuristic/axle_load/candidate_item_axle_score.js +42 -0
- package/src/Allocation_Optimization/item_consumption_heuristic/axle_load/candidate_item_fits_axle_load.js +19 -0
- package/src/Allocation_Optimization/item_consumption_heuristic/axle_load/clone_env_with_layout_items.js +26 -0
- package/src/Allocation_Optimization/item_consumption_heuristic/axle_load/evaluate_layout_axle_load.js +63 -0
- package/src/Allocation_Optimization/item_consumption_heuristic/axle_load/layout_fits_axle_load.js +36 -0
- package/src/Allocation_Optimization/item_consumption_heuristic/axle_load/map_allocated_item_to_axle_wire.js +21 -0
- package/src/Allocation_Optimization/item_consumption_heuristic/select_axle_balanced_slot_item.js +143 -0
- package/src/Allocation_Optimization/item_consumption_heuristic/select_slot_item.js +149 -0
- package/src/Allocation_Optimization/item_consumption_heuristic/slot_candidate_acceptance.js +110 -0
- package/src/Allocation_Optimization/item_consumption_heuristic/slot_candidate_order.js +49 -0
- package/src/models/Cargo_Profile.js +27 -0
- package/src/models/transport/Container.js +11 -0
- package/src/models/transport/Transport.js +12 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const layout_fits_axle_load = require('./layout_fits_axle_load')
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Whether placing this allocated item on top of pending axle items stays within axle caps.
|
|
5
|
+
* @param {object} data - Chooser bag with transport, container, pending_axle_items.
|
|
6
|
+
* @param {object} allocated_item
|
|
7
|
+
* @returns {boolean}
|
|
8
|
+
*/
|
|
9
|
+
function candidate_item_fits_axle_load(data, allocated_item)
|
|
10
|
+
{
|
|
11
|
+
const pending_axle_items = data.pending_axle_items ?? []
|
|
12
|
+
return layout_fits_axle_load({
|
|
13
|
+
transport: data.transport,
|
|
14
|
+
container: data.container,
|
|
15
|
+
extra_items: pending_axle_items.concat(allocated_item)
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = candidate_item_fits_axle_load
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const clone = require('../../../tools/Clone')
|
|
2
|
+
const map_allocated_item_to_axle_wire = require('./map_allocated_item_to_axle_wire')
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Deep-clones the axle env template and writes current plus extra items per container.
|
|
6
|
+
* @param {object} transport
|
|
7
|
+
* @param {object} current_container
|
|
8
|
+
* @param {object[]} extra_items - Pending wall, earlier slots, and candidate for current container.
|
|
9
|
+
* @returns {object}
|
|
10
|
+
*/
|
|
11
|
+
function clone_env_with_layout_items(transport, current_container, extra_items)
|
|
12
|
+
{
|
|
13
|
+
const env = clone(transport.get_axle_env_template())
|
|
14
|
+
const extra_item_list = extra_items ?? []
|
|
15
|
+
for (const container_data of env.containers ?? [])
|
|
16
|
+
{
|
|
17
|
+
const sequence = Number(container_data.sequencia)
|
|
18
|
+
const live_container = transport.get_containers().get(sequence)
|
|
19
|
+
const committed_items = live_container?.get_allocated_items?.() ?? []
|
|
20
|
+
const pending_items = live_container === current_container ? extra_item_list : []
|
|
21
|
+
container_data.items = committed_items.concat(pending_items).map(map_allocated_item_to_axle_wire)
|
|
22
|
+
}
|
|
23
|
+
return env
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = clone_env_with_layout_items
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const { calculate_weight_on_axles_physics } = require('@jetta/axle-load-calculator')
|
|
2
|
+
const { gt } = require('@jetta/number-compare')
|
|
3
|
+
const { Logger } = require('@jetta/logger')
|
|
4
|
+
const Cargo_Profile = require('../../../models/Cargo_Profile')
|
|
5
|
+
const clone_env_with_layout_items = require('./clone_env_with_layout_items')
|
|
6
|
+
const axle_load_exceeds_limit = require('./axle_load_exceeds_limit')
|
|
7
|
+
const { max_axle_load_ratio } = axle_load_exceeds_limit
|
|
8
|
+
|
|
9
|
+
const logger = new Logger('Axle Load Slot Gate')
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Hottest axle ratio for the layout, or undefined when physics cannot run.
|
|
13
|
+
* @param {object} data
|
|
14
|
+
* @param {object} data.transport
|
|
15
|
+
* @param {object} data.container
|
|
16
|
+
* @param {object[]} data.extra_items
|
|
17
|
+
* @returns {number|undefined}
|
|
18
|
+
*/
|
|
19
|
+
function score_layout_axle_load(data)
|
|
20
|
+
{
|
|
21
|
+
const env = clone_env_with_layout_items(data.transport, data.container, data.extra_items)
|
|
22
|
+
try
|
|
23
|
+
{
|
|
24
|
+
return ratio_from_physics(env)
|
|
25
|
+
}
|
|
26
|
+
catch (error)
|
|
27
|
+
{
|
|
28
|
+
logger.warn(`axle calculator rejected layout: ${error.message}`)
|
|
29
|
+
return undefined
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Runs axle physics for the current layout plus extra items on the active container.
|
|
35
|
+
* @param {object} data
|
|
36
|
+
* @param {object} data.transport
|
|
37
|
+
* @param {object} data.container
|
|
38
|
+
* @param {object[]} data.extra_items
|
|
39
|
+
* @returns {boolean} True when no axle exceeds the folga-adjusted cap.
|
|
40
|
+
*/
|
|
41
|
+
function evaluate_layout_axle_load(data)
|
|
42
|
+
{
|
|
43
|
+
const ratio = score_layout_axle_load(data)
|
|
44
|
+
if (ratio === undefined)
|
|
45
|
+
{
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
return gt(ratio, 1) === false
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {object} env
|
|
53
|
+
* @returns {number}
|
|
54
|
+
*/
|
|
55
|
+
function ratio_from_physics(env)
|
|
56
|
+
{
|
|
57
|
+
const output = calculate_weight_on_axles_physics({ envs: [env] })
|
|
58
|
+
const folga_percent = Cargo_Profile.axle_weight_gap_percent()
|
|
59
|
+
return max_axle_load_ratio(output.envs[0], folga_percent)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = evaluate_layout_axle_load
|
|
63
|
+
module.exports.score_layout_axle_load = score_layout_axle_load
|
package/src/Allocation_Optimization/item_consumption_heuristic/axle_load/layout_fits_axle_load.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const Cargo_Profile = require('../../../models/Cargo_Profile')
|
|
2
|
+
const evaluate_layout_axle_load = require('./evaluate_layout_axle_load')
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Returns whether the layout may be committed under the optional axle slot gate.
|
|
6
|
+
* @param {object} data
|
|
7
|
+
* @param {object} [data.transport]
|
|
8
|
+
* @param {object} [data.container]
|
|
9
|
+
* @param {object[]} [data.extra_items]
|
|
10
|
+
* @returns {boolean}
|
|
11
|
+
*/
|
|
12
|
+
function layout_fits_axle_load(data)
|
|
13
|
+
{
|
|
14
|
+
if (!Cargo_Profile.enable_axle_load_slot_gate())
|
|
15
|
+
{
|
|
16
|
+
return true
|
|
17
|
+
}
|
|
18
|
+
if (!transport_has_axles(data.transport))
|
|
19
|
+
{
|
|
20
|
+
return true
|
|
21
|
+
}
|
|
22
|
+
return evaluate_layout_axle_load(data)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {object|undefined} transport
|
|
27
|
+
* @returns {boolean}
|
|
28
|
+
*/
|
|
29
|
+
function transport_has_axles(transport)
|
|
30
|
+
{
|
|
31
|
+
const env = transport?.get_axle_env_template?.()
|
|
32
|
+
return Array.isArray(env?.eixos) && env.eixos.length > 0
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = layout_fits_axle_load
|
|
36
|
+
module.exports.transport_has_axles = transport_has_axles
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps an allocated item to the ms-faixas axle-calculator item contract.
|
|
3
|
+
* Longitudinal chassis position is JSON `y` (allocator `z`).
|
|
4
|
+
* @param {object} allocated_item
|
|
5
|
+
* @returns {object}
|
|
6
|
+
*/
|
|
7
|
+
function map_allocated_item_to_axle_wire(allocated_item)
|
|
8
|
+
{
|
|
9
|
+
return {
|
|
10
|
+
id: allocated_item.get_id(),
|
|
11
|
+
x: allocated_item.get_x(),
|
|
12
|
+
y: allocated_item.get_z(),
|
|
13
|
+
z: allocated_item.get_y(),
|
|
14
|
+
peso: allocated_item.get_weight(),
|
|
15
|
+
comprimento: allocated_item.get_length(),
|
|
16
|
+
largura: allocated_item.get_width(),
|
|
17
|
+
altura: allocated_item.get_height()
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = map_allocated_item_to_axle_wire
|
package/src/Allocation_Optimization/item_consumption_heuristic/select_axle_balanced_slot_item.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
const { lt } = require('@jetta/number-compare')
|
|
2
|
+
const { get_item, remove_item_from_candidate_list } = require('./slot_candidate_order')
|
|
3
|
+
const { score_without_commit, commit_scored_candidate } = require('./slot_candidate_acceptance')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Legal SKU across every weight band with the lowest hottest-axle ratio.
|
|
7
|
+
* Ties keep band order, then candidate order inside the band.
|
|
8
|
+
* @param {object} data
|
|
9
|
+
* @param {object} context
|
|
10
|
+
* @returns {object|undefined}
|
|
11
|
+
*/
|
|
12
|
+
function commit_lowest_ratio_across_bands(data, context)
|
|
13
|
+
{
|
|
14
|
+
const ranked = rank_every_band(data, context)
|
|
15
|
+
for (const selected of ranked)
|
|
16
|
+
{
|
|
17
|
+
const committed = commit_scored_candidate(data, context.container, selected)
|
|
18
|
+
if (committed)
|
|
19
|
+
{
|
|
20
|
+
return committed
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return undefined
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Best legal SKU across every band. Does not commit stacking.
|
|
28
|
+
* @param {object} data
|
|
29
|
+
* @param {object} context
|
|
30
|
+
* @returns {{ allocated_item: object, ratio: number, allocation_item: object }|undefined}
|
|
31
|
+
*/
|
|
32
|
+
function lowest_ratio_across_bands(data, context)
|
|
33
|
+
{
|
|
34
|
+
const ranked = rank_every_band(data, context)
|
|
35
|
+
return ranked[0]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Preferred SKU scored without a stacking commit.
|
|
40
|
+
* @param {object} data
|
|
41
|
+
* @param {object} context
|
|
42
|
+
* @returns {{ allocated_item: object, ratio: number, allocation_item: object }|undefined}
|
|
43
|
+
*/
|
|
44
|
+
function score_preferred(data, context)
|
|
45
|
+
{
|
|
46
|
+
const allocation_item = data.item_list.find(item => item.get_id() === data.previous_chosen_item_id)
|
|
47
|
+
if (!allocation_item)
|
|
48
|
+
{
|
|
49
|
+
return undefined
|
|
50
|
+
}
|
|
51
|
+
return score_without_commit(data, context, allocation_item)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {{ ratio: number }|undefined} preferred
|
|
56
|
+
* @param {{ ratio: number }|undefined} best
|
|
57
|
+
* @returns {boolean}
|
|
58
|
+
*/
|
|
59
|
+
function preferred_wins(preferred, best)
|
|
60
|
+
{
|
|
61
|
+
if (!preferred)
|
|
62
|
+
{
|
|
63
|
+
return false
|
|
64
|
+
}
|
|
65
|
+
if (!best)
|
|
66
|
+
{
|
|
67
|
+
return true
|
|
68
|
+
}
|
|
69
|
+
return lt(best.ratio, preferred.ratio) === false
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {object} data
|
|
74
|
+
* @param {object} context
|
|
75
|
+
* @returns {object[]}
|
|
76
|
+
*/
|
|
77
|
+
function rank_every_band(data, context)
|
|
78
|
+
{
|
|
79
|
+
const scored = []
|
|
80
|
+
for (const band of context.ordered_bands)
|
|
81
|
+
{
|
|
82
|
+
scored.push(...rank_band(data, context, band.items.slice()))
|
|
83
|
+
}
|
|
84
|
+
return rank_by_ratio(scored)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {object} data
|
|
89
|
+
* @param {object} context
|
|
90
|
+
* @param {object[]} pending
|
|
91
|
+
* @returns {object[]}
|
|
92
|
+
*/
|
|
93
|
+
function rank_band(data, context, pending)
|
|
94
|
+
{
|
|
95
|
+
const scored = []
|
|
96
|
+
while (pending.length > 0)
|
|
97
|
+
{
|
|
98
|
+
const allocation_item = get_item(pending, data.qty_of_slots, context.container)
|
|
99
|
+
const candidate = score_without_commit(data, context, allocation_item)
|
|
100
|
+
if (candidate)
|
|
101
|
+
{
|
|
102
|
+
scored.push(candidate)
|
|
103
|
+
}
|
|
104
|
+
remove_item_from_candidate_list(pending, allocation_item)
|
|
105
|
+
}
|
|
106
|
+
return rank_by_ratio(scored)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param {{ ratio: number }[]} scored
|
|
111
|
+
* @returns {{ ratio: number }[]}
|
|
112
|
+
*/
|
|
113
|
+
function rank_by_ratio(scored)
|
|
114
|
+
{
|
|
115
|
+
const ranked = []
|
|
116
|
+
for (const candidate of scored)
|
|
117
|
+
{
|
|
118
|
+
insert_by_ratio(ranked, candidate)
|
|
119
|
+
}
|
|
120
|
+
return ranked
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @param {{ ratio: number }[]} ranked
|
|
125
|
+
* @param {{ ratio: number }} candidate
|
|
126
|
+
* @returns {void}
|
|
127
|
+
*/
|
|
128
|
+
function insert_by_ratio(ranked, candidate)
|
|
129
|
+
{
|
|
130
|
+
let index = 0
|
|
131
|
+
while (index < ranked.length && lt(ranked[index].ratio, candidate.ratio))
|
|
132
|
+
{
|
|
133
|
+
index += 1
|
|
134
|
+
}
|
|
135
|
+
ranked.splice(index, 0, candidate)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = {
|
|
139
|
+
commit_lowest_ratio_across_bands,
|
|
140
|
+
lowest_ratio_across_bands,
|
|
141
|
+
score_preferred,
|
|
142
|
+
preferred_wins
|
|
143
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
const Cargo_Profile = require('../../models/Cargo_Profile')
|
|
2
|
+
const No_Suitable_Item_Exception = require('../block_builder/exceptions/No_Suitable_Item_Exception')
|
|
3
|
+
const { ALLOCATOR_MESSAGE } = require('../../messages/Allocator_Message')
|
|
4
|
+
const { distribute_items_by_weight_range } = require('./Weight_Slot_Distribution')
|
|
5
|
+
const {
|
|
6
|
+
resolve_choose_item_heuristic_context,
|
|
7
|
+
format_slot_label_for_exception
|
|
8
|
+
} = require('./Choose_Item_Heuristic_Context')
|
|
9
|
+
const {
|
|
10
|
+
calculate_total_stack_slots,
|
|
11
|
+
sort_stacking_slots_by_proximity
|
|
12
|
+
} = require('./Stack_Slot_Calculator')
|
|
13
|
+
const { get_item, remove_item_from_candidate_list } = require('./slot_candidate_order')
|
|
14
|
+
const { try_accept_candidate, commit_scored_candidate } = require('./slot_candidate_acceptance')
|
|
15
|
+
const {
|
|
16
|
+
commit_lowest_ratio_across_bands,
|
|
17
|
+
lowest_ratio_across_bands,
|
|
18
|
+
score_preferred,
|
|
19
|
+
preferred_wins
|
|
20
|
+
} = require('./select_axle_balanced_slot_item')
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Chooses a slot item. When the axle gate is on, the legal SKU with the lowest hottest-axle ratio wins.
|
|
24
|
+
* @param {object} data
|
|
25
|
+
* @returns {object}
|
|
26
|
+
*/
|
|
27
|
+
function select_slot_item(data)
|
|
28
|
+
{
|
|
29
|
+
const context = build_band_context(data)
|
|
30
|
+
if (Cargo_Profile.enable_axle_load_slot_gate())
|
|
31
|
+
{
|
|
32
|
+
const selected = commit_lowest_ratio_across_bands(data, context)
|
|
33
|
+
if (!selected)
|
|
34
|
+
{
|
|
35
|
+
throw unsuitable(context.slot)
|
|
36
|
+
}
|
|
37
|
+
return selected
|
|
38
|
+
}
|
|
39
|
+
for (const band of context.ordered_bands)
|
|
40
|
+
{
|
|
41
|
+
const selected = first_accepted_in_band(data, context, band.items)
|
|
42
|
+
if (selected)
|
|
43
|
+
{
|
|
44
|
+
return selected
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
throw unsuitable(context.slot)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Same-id preference. Under the axle gate, that SKU wins only when it is not strictly hotter.
|
|
52
|
+
* @param {object} data
|
|
53
|
+
* @returns {object}
|
|
54
|
+
*/
|
|
55
|
+
function select_preferred_or_heuristic(data)
|
|
56
|
+
{
|
|
57
|
+
if (!Cargo_Profile.enable_axle_load_slot_gate())
|
|
58
|
+
{
|
|
59
|
+
return preferred_or_first(data)
|
|
60
|
+
}
|
|
61
|
+
return preferred_or_balanced(data)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {object} data
|
|
66
|
+
* @returns {object}
|
|
67
|
+
*/
|
|
68
|
+
function preferred_or_first(data)
|
|
69
|
+
{
|
|
70
|
+
const { container, item_list, previous_chosen_item_id, item_position } = data
|
|
71
|
+
const allocation_item = item_list.find(item => item.get_id() === previous_chosen_item_id)
|
|
72
|
+
if (allocation_item)
|
|
73
|
+
{
|
|
74
|
+
const accepted_item = try_accept_candidate(data, container, allocation_item, item_position)
|
|
75
|
+
if (accepted_item)
|
|
76
|
+
{
|
|
77
|
+
return accepted_item
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return select_slot_item(data)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {object} data
|
|
85
|
+
* @returns {object}
|
|
86
|
+
*/
|
|
87
|
+
function preferred_or_balanced(data)
|
|
88
|
+
{
|
|
89
|
+
const context = build_band_context(data)
|
|
90
|
+
const preferred = score_preferred(data, context)
|
|
91
|
+
const best = lowest_ratio_across_bands(data, context)
|
|
92
|
+
const winner = preferred_wins(preferred, best) ? preferred : best
|
|
93
|
+
if (!winner)
|
|
94
|
+
{
|
|
95
|
+
throw unsuitable(context.slot)
|
|
96
|
+
}
|
|
97
|
+
return commit_scored_candidate(data, context.container, winner)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @param {object} data
|
|
102
|
+
* @returns {object}
|
|
103
|
+
*/
|
|
104
|
+
function build_band_context(data)
|
|
105
|
+
{
|
|
106
|
+
const { slot, container, item_position } = resolve_choose_item_heuristic_context(data)
|
|
107
|
+
const total_slots = calculate_total_stack_slots(container.get_height(), slot)
|
|
108
|
+
const stacking_slots = distribute_items_by_weight_range(data.item_list, total_slots)
|
|
109
|
+
const ordered_bands = sort_stacking_slots_by_proximity(stacking_slots, slot.get_y(), slot.get_height())
|
|
110
|
+
return { slot, container, item_position, ordered_bands }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {object} data
|
|
115
|
+
* @param {object} context
|
|
116
|
+
* @param {object[]} candidate_items
|
|
117
|
+
* @returns {object|undefined}
|
|
118
|
+
*/
|
|
119
|
+
function first_accepted_in_band(data, context, candidate_items)
|
|
120
|
+
{
|
|
121
|
+
while (candidate_items.length > 0)
|
|
122
|
+
{
|
|
123
|
+
const allocation_item = get_item(candidate_items, data.qty_of_slots, context.container)
|
|
124
|
+
const accepted_item = try_accept_candidate(data, context.container, allocation_item, context.item_position)
|
|
125
|
+
if (accepted_item)
|
|
126
|
+
{
|
|
127
|
+
return accepted_item
|
|
128
|
+
}
|
|
129
|
+
remove_item_from_candidate_list(candidate_items, allocation_item)
|
|
130
|
+
}
|
|
131
|
+
return undefined
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* @param {object} slot
|
|
136
|
+
* @returns {No_Suitable_Item_Exception}
|
|
137
|
+
*/
|
|
138
|
+
function unsuitable(slot)
|
|
139
|
+
{
|
|
140
|
+
return new No_Suitable_Item_Exception(
|
|
141
|
+
ALLOCATOR_MESSAGE.NO_SUITABLE_ITEM,
|
|
142
|
+
[format_slot_label_for_exception(slot)]
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = {
|
|
147
|
+
select_slot_item,
|
|
148
|
+
select_preferred_or_heuristic
|
|
149
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const Cargo_Profile = require('../../models/Cargo_Profile')
|
|
2
|
+
const Allocated_Item = require('../../models/Allocated_Item')
|
|
3
|
+
const candidate_item_fits_axle_load = require('./axle_load/candidate_item_fits_axle_load')
|
|
4
|
+
const candidate_item_axle_score = require('./axle_load/candidate_item_axle_score')
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Builds the allocated item at the slot position.
|
|
8
|
+
* @param {object} allocation_item
|
|
9
|
+
* @param {object} item_position
|
|
10
|
+
* @returns {object}
|
|
11
|
+
*/
|
|
12
|
+
function build_allocated_item(allocation_item, item_position)
|
|
13
|
+
{
|
|
14
|
+
return new Allocated_Item(allocation_item, item_position.x, item_position.y, item_position.z)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Accepts the first legal candidate and commits stacking.
|
|
19
|
+
* @param {object} data
|
|
20
|
+
* @param {object} container
|
|
21
|
+
* @param {object} allocation_item
|
|
22
|
+
* @param {object} item_position
|
|
23
|
+
* @returns {object|undefined}
|
|
24
|
+
*/
|
|
25
|
+
function try_accept_candidate(data, container, allocation_item, item_position)
|
|
26
|
+
{
|
|
27
|
+
const allocated_item = build_allocated_item(allocation_item, item_position)
|
|
28
|
+
if (!candidate_item_fits_axle_load(data, allocated_item))
|
|
29
|
+
{
|
|
30
|
+
return undefined
|
|
31
|
+
}
|
|
32
|
+
if (should_defer_loose_on_block_stacking_validation(data.block))
|
|
33
|
+
{
|
|
34
|
+
return allocated_item
|
|
35
|
+
}
|
|
36
|
+
if (!container.is_a_valid_stacking(allocated_item))
|
|
37
|
+
{
|
|
38
|
+
return undefined
|
|
39
|
+
}
|
|
40
|
+
return allocated_item
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Scores a candidate without writing the stacking index.
|
|
45
|
+
* @param {object} data
|
|
46
|
+
* @param {object} context
|
|
47
|
+
* @param {object} allocation_item
|
|
48
|
+
* @returns {{ allocated_item: object, ratio: number, allocation_item: object }|undefined}
|
|
49
|
+
*/
|
|
50
|
+
function score_without_commit(data, context, allocation_item)
|
|
51
|
+
{
|
|
52
|
+
const allocated_item = build_allocated_item(allocation_item, context.item_position)
|
|
53
|
+
const score = candidate_item_axle_score(data, allocated_item)
|
|
54
|
+
if (!score.fits || !stacking_probe_allows(data, context.container, allocated_item))
|
|
55
|
+
{
|
|
56
|
+
return undefined
|
|
57
|
+
}
|
|
58
|
+
return { allocated_item, ratio: score.ratio, allocation_item }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Commits the chosen candidate into the live stacking index.
|
|
63
|
+
* @param {object} data
|
|
64
|
+
* @param {object} container
|
|
65
|
+
* @param {{ allocated_item: object }} scored
|
|
66
|
+
* @returns {object|undefined}
|
|
67
|
+
*/
|
|
68
|
+
function commit_scored_candidate(data, container, scored)
|
|
69
|
+
{
|
|
70
|
+
if (should_defer_loose_on_block_stacking_validation(data.block))
|
|
71
|
+
{
|
|
72
|
+
return scored.allocated_item
|
|
73
|
+
}
|
|
74
|
+
if (!container.is_a_valid_stacking(scored.allocated_item))
|
|
75
|
+
{
|
|
76
|
+
return undefined
|
|
77
|
+
}
|
|
78
|
+
return scored.allocated_item
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {object} data
|
|
83
|
+
* @param {object} container
|
|
84
|
+
* @param {object} allocated_item
|
|
85
|
+
* @returns {boolean}
|
|
86
|
+
*/
|
|
87
|
+
function stacking_probe_allows(data, container, allocated_item)
|
|
88
|
+
{
|
|
89
|
+
if (should_defer_loose_on_block_stacking_validation(data.block))
|
|
90
|
+
{
|
|
91
|
+
return true
|
|
92
|
+
}
|
|
93
|
+
if (typeof container.probe_stacking === 'function')
|
|
94
|
+
{
|
|
95
|
+
return container.probe_stacking(allocated_item)
|
|
96
|
+
}
|
|
97
|
+
return container.is_a_valid_stacking(allocated_item)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function should_defer_loose_on_block_stacking_validation(block)
|
|
101
|
+
{
|
|
102
|
+
return Cargo_Profile.stack_loose_on_tie_ratio_blocks()
|
|
103
|
+
&& block?.get_slot_list?.().length === 1
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = {
|
|
107
|
+
try_accept_candidate,
|
|
108
|
+
score_without_commit,
|
|
109
|
+
commit_scored_candidate
|
|
110
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const No_Available_Items_Exception = require('./exceptions/No_Available_Items_Exception')
|
|
2
|
+
const No_Slots_For_Items_Exception = require('./exceptions/No_Slots_For_Items_Exception')
|
|
3
|
+
const { order_items_by_capacity_bias } = require('./Capacity_Bias_Item_Order')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Removes one candidate from the working list.
|
|
7
|
+
* @param {object[]} candidate_item_list
|
|
8
|
+
* @param {object} allocation_item
|
|
9
|
+
* @returns {void}
|
|
10
|
+
*/
|
|
11
|
+
function remove_item_from_candidate_list(candidate_item_list, allocation_item)
|
|
12
|
+
{
|
|
13
|
+
const index = candidate_item_list.indexOf(allocation_item)
|
|
14
|
+
if (index > -1)
|
|
15
|
+
{
|
|
16
|
+
candidate_item_list.splice(index, 1)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Next candidate in the existing capacity-bias order.
|
|
22
|
+
* @param {object[]} item_list
|
|
23
|
+
* @param {number|undefined} qty_of_slots
|
|
24
|
+
* @param {object} container
|
|
25
|
+
* @returns {object}
|
|
26
|
+
*/
|
|
27
|
+
function get_item(item_list, qty_of_slots, container)
|
|
28
|
+
{
|
|
29
|
+
if (qty_of_slots === undefined)
|
|
30
|
+
{
|
|
31
|
+
throw new No_Slots_For_Items_Exception()
|
|
32
|
+
}
|
|
33
|
+
if (item_list.length === 0)
|
|
34
|
+
{
|
|
35
|
+
throw new No_Available_Items_Exception()
|
|
36
|
+
}
|
|
37
|
+
const ordered_item_list = order_items_by_capacity_bias(item_list, container)
|
|
38
|
+
const preferable_item_list = ordered_item_list.filter(item => item.get_amount() >= qty_of_slots)
|
|
39
|
+
if (preferable_item_list.length > 0)
|
|
40
|
+
{
|
|
41
|
+
return preferable_item_list[0]
|
|
42
|
+
}
|
|
43
|
+
return ordered_item_list[0]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = {
|
|
47
|
+
get_item,
|
|
48
|
+
remove_item_from_candidate_list
|
|
49
|
+
}
|
|
@@ -81,6 +81,33 @@ class Cargo_Profile
|
|
|
81
81
|
return str_to_bool(this.#instance['allocation.blocks.enable_item_continuation_lock'], true)
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Whether slot item choice rejects SKUs that would overload axles.
|
|
86
|
+
* Defaults to false when omitted or when no cargo profile is loaded.
|
|
87
|
+
* @returns {boolean}
|
|
88
|
+
*/
|
|
89
|
+
static enable_axle_load_slot_gate()
|
|
90
|
+
{
|
|
91
|
+
if (Cargo_Profile.#instance === null)
|
|
92
|
+
{
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
return str_to_bool(this.#instance['allocation.blocks.enable_axle_load_slot_gate'], false)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Axle slack percent applied as pesoMaxFinal * (1 + percent / 100).
|
|
100
|
+
* @returns {number}
|
|
101
|
+
*/
|
|
102
|
+
static axle_weight_gap_percent()
|
|
103
|
+
{
|
|
104
|
+
if (Cargo_Profile.#instance === null)
|
|
105
|
+
{
|
|
106
|
+
return 0
|
|
107
|
+
}
|
|
108
|
+
return finite_float_from_string_or_number(this.#instance.folga_de_peso_nos_eixos, 0)
|
|
109
|
+
}
|
|
110
|
+
|
|
84
111
|
static enable_key_item_mapping()
|
|
85
112
|
{
|
|
86
113
|
return str_to_bool(this.#instance['allocation.blocks.preferential.enable'], true)
|
|
@@ -87,6 +87,17 @@ class Container extends Space
|
|
|
87
87
|
return this.#stacking_feasibility_checker.validate_stackings(item_data)
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Checks stacking on a clone so the live index stays unchanged.
|
|
92
|
+
* @param {object} item
|
|
93
|
+
* @returns {boolean}
|
|
94
|
+
*/
|
|
95
|
+
probe_stacking(item)
|
|
96
|
+
{
|
|
97
|
+
const probe = this.#stacking_feasibility_checker.clone()
|
|
98
|
+
return probe.validate_stackings(create_input(item))
|
|
99
|
+
}
|
|
100
|
+
|
|
90
101
|
/**
|
|
91
102
|
* Drops provisional stacking commits left by failed slot/wall attempts.
|
|
92
103
|
* Restores the pre-attempt checkpoint when stacking was touched; otherwise a no-op.
|