package com.zy.asrs.service.impl;
|
|
import com.alibaba.fastjson.JSON;
|
import com.alibaba.fastjson.JSONObject;
|
import com.baomidou.mybatisplus.mapper.EntityWrapper;
|
import com.zy.asrs.entity.BasCrnp;
|
import com.zy.asrs.entity.LocMast;
|
import com.zy.asrs.entity.WrkMast;
|
import com.zy.asrs.planner.PlannerOrtoolsSolverService;
|
import com.zy.asrs.service.*;
|
import com.zy.common.utils.HttpHandler;
|
import com.zy.common.utils.RedisUtil;
|
import com.zy.core.News;
|
import com.zy.core.cache.SlaveConnection;
|
import com.zy.core.enums.RedisKeyType;
|
import com.zy.core.enums.SlaveType;
|
import com.zy.core.enums.WrkStsType;
|
import com.zy.core.model.StationObjModel;
|
import com.zy.core.model.protocol.CrnProtocol;
|
import com.zy.core.model.protocol.StationProtocol;
|
import com.zy.core.thread.CrnThread;
|
import com.zy.core.thread.StationThread;
|
import com.zy.core.utils.StationOperateProcessUtils;
|
import com.zy.system.entity.Config;
|
import com.zy.system.service.ConfigService;
|
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.stereotype.Service;
|
|
import java.util.ArrayList;
|
import java.util.HashMap;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.concurrent.TimeUnit;
|
|
@Service
|
public class PlannerServiceImpl implements PlannerService {
|
|
@Autowired
|
private BasCrnpService basCrnpService;
|
@Autowired
|
private WrkMastService wrkMastService;
|
@Autowired
|
private LocMastService locMastService;
|
@Autowired
|
private ConfigService configService;
|
@Autowired
|
private RedisUtil redisUtil;
|
@Autowired
|
private PlannerOrtoolsSolverService plannerOrtoolsSolverService;
|
@Autowired
|
private StationOperateProcessUtils stationOperateProcessUtils;
|
|
@Override
|
public JSONObject calculateAndSaveSchedule() {
|
ArrayList<HashMap<String, Object>> crnDataList = new ArrayList<>();
|
|
List<BasCrnp> basCrnps = basCrnpService.selectList(new EntityWrapper<BasCrnp>().eq("status", 1));
|
Map<Integer, StationObjModel> stationIndex = new HashMap<>();
|
Map<Integer, String> stationCrnCodeIndex = new HashMap<>();
|
|
Double bayWidth = getDoubleConfig("plannerBayWidthM", 1.0);
|
Double levHeight = getDoubleConfig("plannerLevHeightM", 1.0);
|
|
// First pass: Build station index
|
for (BasCrnp basCrnp : basCrnps) {
|
List<StationObjModel> inStations = basCrnp.getInStationList$();
|
if (inStations != null) {
|
for (StationObjModel stationObjModel : inStations) {
|
Integer sid = stationObjModel.getStationId();
|
if (sid != null) {
|
stationIndex.put(sid, stationObjModel);
|
stationCrnCodeIndex.put(sid, "CRN-" + basCrnp.getCrnNo());
|
}
|
}
|
}
|
List<StationObjModel> outStations = basCrnp.getOutStationList$();
|
if (outStations != null) {
|
for (StationObjModel stationObjModel : outStations) {
|
Integer sid = stationObjModel.getStationId();
|
if (sid != null) {
|
stationIndex.put(sid, stationObjModel);
|
}
|
}
|
}
|
}
|
|
// Second pass: Build crane data
|
for (BasCrnp basCrnp : basCrnps) {
|
CrnThread crnThread = (CrnThread) SlaveConnection.get(SlaveType.Crn, basCrnp.getCrnNo());
|
if (crnThread == null) {
|
continue;
|
}
|
|
HashMap<String, Object> crnData = new HashMap<>();
|
crnData.put("code", "CRN-" + basCrnp.getCrnNo());
|
crnData.put("name", basCrnp.getCrnNo());
|
// 规划时使用额定速度,而非当前实时速度
|
Double vx = getDoubleConfig("plannerVxDefault", 2.6);
|
Double vy = getDoubleConfig("plannerVyDefault", 0.67);
|
crnData.put("vx", vx);
|
crnData.put("vy", vy);
|
try {
|
if (crnThread.getStatus() != null) {
|
Integer bay = crnThread.getStatus().getBay();
|
Integer lev = crnThread.getStatus().getLevel();
|
if (bay != null && lev != null) {
|
HashMap<String, Object> initPos = new HashMap<>();
|
initPos.put("x", bay * bayWidth);
|
initPos.put("y", lev * levHeight);
|
crnData.put("initPos", initPos);
|
}
|
}
|
} catch (Exception ignore) {}
|
Integer pickSec = getIntConfig("plannerPickSec", 8);
|
Integer dropSec = getIntConfig("plannerDropSec", 6);
|
crnData.put("pickSec", pickSec);
|
crnData.put("dropSec", dropSec);
|
crnData.put("moveMode", "max");
|
|
int readySec = 0;
|
try {
|
if (crnThread.getStatus() != null) {
|
CrnProtocol p = crnThread.getStatus();
|
Integer tNo = p.getTaskNo();
|
if (tNo != null && tNo > 0) {
|
WrkMast task = wrkMastService.selectOne(new EntityWrapper<WrkMast>().eq("wrk_no", tNo));
|
if (task != null) {
|
// Calculate current X, Y
|
double curX = (p.getBay() == null ? 0 : p.getBay()) * bayWidth;
|
double curY = (p.getLevel() == null ? 0 : p.getLevel()) * levHeight;
|
|
double targetX = curX;
|
double targetY = curY;
|
boolean hasGoods = (p.getLoaded() != null && p.getLoaded() == 1);
|
|
if (hasGoods) {
|
// Moving to destination
|
if (task.getWrkSts() != null && task.getWrkSts() < 50) { // IN task
|
// For IN task with goods, destination is Location
|
if (task.getLocNo() != null) {
|
LocMast loc = locMastService.queryByLoc(task.getLocNo());
|
if (loc != null) {
|
targetX = (loc.getBay1() == null ? 0 : loc.getBay1()) * bayWidth;
|
targetY = (loc.getLev1() == null ? 0 : loc.getLev1()) * levHeight;
|
}
|
}
|
} else { // OUT or LOC_MOVE
|
if (task.getStaNo() != null) { // OUT task
|
// For OUT task with goods, destination is Station
|
StationObjModel s = stationIndex.get(task.getStaNo());
|
if (s != null) {
|
targetX = (s.getDeviceBay() == null ? 0 : s.getDeviceBay()) * bayWidth;
|
targetY = (s.getDeviceLev() == null ? 0 : s.getDeviceLev()) * levHeight;
|
}
|
} else if (task.getLocNo() != null) { // LOC_MOVE
|
LocMast loc = locMastService.queryByLoc(task.getLocNo());
|
if (loc != null) {
|
targetX = (loc.getBay1() == null ? 0 : loc.getBay1()) * bayWidth;
|
targetY = (loc.getLev1() == null ? 0 : loc.getLev1()) * levHeight;
|
}
|
}
|
}
|
} else {
|
// Moving to source
|
if (task.getWrkSts() != null && task.getWrkSts() < 50) { // IN task
|
StationObjModel s = stationIndex.get(task.getStaNo());
|
if (s != null) {
|
targetX = (s.getDeviceBay() == null ? 0 : s.getDeviceBay()) * bayWidth;
|
targetY = (s.getDeviceLev() == null ? 0 : s.getDeviceLev()) * levHeight;
|
}
|
} else { // OUT or LOC_MOVE
|
if (task.getSourceLocNo() != null) {
|
LocMast loc = locMastService.queryByLoc(task.getSourceLocNo());
|
if (loc != null) {
|
targetX = (loc.getBay1() == null ? 0 : loc.getBay1()) * bayWidth;
|
targetY = (loc.getLev1() == null ? 0 : loc.getLev1()) * levHeight;
|
}
|
}
|
}
|
}
|
|
double dx = Math.abs(targetX - curX);
|
double dy = Math.abs(targetY - curY);
|
double tx = vx <= 0 ? 0 : dx / vx;
|
double ty = vy <= 0 ? 0 : dy / vy;
|
double moveTime = Math.max(tx, ty);
|
|
double total = moveTime;
|
if (hasGoods) {
|
total += dropSec;
|
} else {
|
total += pickSec;
|
// Plus time to move to destination + drop
|
double srcX = targetX;
|
double srcY = targetY;
|
double dstX = srcX;
|
double dstY = srcY;
|
|
if (task.getWrkSts() != null && task.getWrkSts() < 50) { // Inbound
|
if (task.getLocNo() != null) {
|
LocMast loc = locMastService.queryByLoc(task.getLocNo());
|
if (loc != null) {
|
dstX = (loc.getBay1() == null ? 0 : loc.getBay1()) * bayWidth;
|
dstY = (loc.getLev1() == null ? 0 : loc.getLev1()) * levHeight;
|
}
|
}
|
} else { // Outbound or LocMove
|
if (task.getStaNo() != null) {
|
StationObjModel s = stationIndex.get(task.getStaNo());
|
if (s != null) {
|
dstX = (s.getDeviceBay() == null ? 0 : s.getDeviceBay()) * bayWidth;
|
dstY = (s.getDeviceLev() == null ? 0 : s.getDeviceLev()) * levHeight;
|
}
|
} else if (task.getLocNo() != null) {
|
LocMast loc = locMastService.queryByLoc(task.getLocNo());
|
if (loc != null) {
|
dstX = (loc.getBay1() == null ? 0 : loc.getBay1()) * bayWidth;
|
dstY = (loc.getLev1() == null ? 0 : loc.getLev1()) * levHeight;
|
}
|
}
|
}
|
|
double d2x = Math.abs(dstX - srcX);
|
double d2y = Math.abs(dstY - srcY);
|
double t2x = vx <= 0 ? 0 : d2x / vx;
|
double t2y = vy <= 0 ? 0 : d2y / vy;
|
total += Math.max(t2x, t2y);
|
total += dropSec;
|
}
|
readySec = (int) Math.ceil(total);
|
}
|
}
|
}
|
} catch (Exception ignore) {}
|
crnData.put("readySec", readySec);
|
crnDataList.add(crnData);
|
}
|
|
ArrayList<HashMap<String, Object>> taskDataList = new ArrayList<>();
|
Map<String, Integer> outGroupSeqCounter = new HashMap<>();
|
List<WrkMast> outTasks = wrkMastService.selectList(new EntityWrapper<WrkMast>().eq("wrk_sts", WrkStsType.NEW_OUTBOUND.sts));
|
for (WrkMast wrkMast : outTasks) {
|
HashMap<String, Object> t = new HashMap<>();
|
t.put("taskId", wrkMast.getWrkNo());
|
t.put("taskType", "OUT");
|
t.put("priority", wrkMast.getIoPri() == null ? 5 : wrkMast.getIoPri().intValue());
|
if (wrkMast.getAppeTime() != null) {
|
t.put("createTime", wrkMast.getAppeTime().getTime());
|
}
|
String src = wrkMast.getSourceLocNo();
|
LocMast srcLoc = src == null ? null : locMastService.queryByLoc(src);
|
int srcX = srcLoc == null ? 0 : (srcLoc.getBay1() == null ? 0 : srcLoc.getBay1());
|
int srcY = srcLoc == null ? 0 : (srcLoc.getLev1() == null ? 0 : srcLoc.getLev1());
|
HashMap<String, Object> fromPos = new HashMap<>();
|
fromPos.put("x", srcX * bayWidth);
|
fromPos.put("y", srcY * levHeight);
|
t.put("fromPos", fromPos);
|
Integer targetStationId = wrkMast.getStaNo();
|
double toX = 0;
|
double toY = 0;
|
try {
|
if (targetStationId != null) {
|
StationObjModel s = stationIndex.get(targetStationId);
|
if (s != null) {
|
toX = s.getDeviceBay() == null ? 0 : s.getDeviceBay();
|
toY = s.getDeviceLev() == null ? 0 : s.getDeviceLev();
|
}
|
}
|
} catch (Exception ignore) {}
|
HashMap<String, Object> toPos = new HashMap<>();
|
toPos.put("x", toX * bayWidth);
|
toPos.put("y", toY * levHeight);
|
t.put("toPos", toPos);
|
ArrayList<String> eligible = new ArrayList<>();
|
Integer crnNo = wrkMast.getCrnNo();
|
if (crnNo != null && crnNo > 0) {
|
eligible.add("CRN-" + crnNo);
|
} else {
|
try {
|
String locNo = src;
|
if (locNo != null) {
|
Integer row = com.zy.asrs.utils.Utils.getRow(locNo);
|
for (BasCrnp basCrnp : basCrnps) {
|
List<List<Integer>> controlRows = basCrnp.getControlRows$();
|
for (List<Integer> rows : controlRows) {
|
if (rows.contains(row)) {
|
eligible.add("CRN-" + basCrnp.getCrnNo());
|
break;
|
}
|
}
|
}
|
}
|
} catch (Exception ignore) {}
|
}
|
t.put("eligibleCranes", eligible);
|
t.put("conveyorPath", new ArrayList<>());
|
String group = wrkMast.getWmsWrkNo();
|
if (group == null || group.trim().isEmpty()) {
|
group = "WCS";
|
}
|
int seq = outGroupSeqCounter.getOrDefault(group, 0) + 1;
|
outGroupSeqCounter.put(group, seq);
|
t.put("outGroup", group);
|
t.put("outSeq", seq);
|
taskDataList.add(t);
|
}
|
|
List<WrkMast> moveTasks = wrkMastService.selectList(new EntityWrapper<WrkMast>().eq("wrk_sts", WrkStsType.NEW_LOC_MOVE.sts));
|
for (WrkMast wrkMast : moveTasks) {
|
HashMap<String, Object> t = new HashMap<>();
|
t.put("taskId", wrkMast.getWrkNo());
|
t.put("taskType", "MOVE");
|
t.put("priority", wrkMast.getIoPri() == null ? 5 : wrkMast.getIoPri().intValue());
|
if (wrkMast.getAppeTime() != null) {
|
t.put("createTime", wrkMast.getAppeTime().getTime());
|
}
|
|
String src = wrkMast.getSourceLocNo();
|
LocMast srcLoc = src == null ? null : locMastService.queryByLoc(src);
|
int srcX = srcLoc == null ? 0 : (srcLoc.getBay1() == null ? 0 : srcLoc.getBay1());
|
int srcY = srcLoc == null ? 0 : (srcLoc.getLev1() == null ? 0 : srcLoc.getLev1());
|
HashMap<String, Object> fromPos = new HashMap<>();
|
fromPos.put("x", srcX * bayWidth);
|
fromPos.put("y", srcY * levHeight);
|
t.put("fromPos", fromPos);
|
|
String dst = wrkMast.getLocNo();
|
LocMast dstLoc = dst == null ? null : locMastService.queryByLoc(dst);
|
int toX = dstLoc == null ? 0 : (dstLoc.getBay1() == null ? 0 : dstLoc.getBay1());
|
int toY = dstLoc == null ? 0 : (dstLoc.getLev1() == null ? 0 : dstLoc.getLev1());
|
HashMap<String, Object> toPos = new HashMap<>();
|
toPos.put("x", toX * bayWidth);
|
toPos.put("y", toY * levHeight);
|
t.put("toPos", toPos);
|
|
ArrayList<String> eligible = new ArrayList<>();
|
Integer crnNo = wrkMast.getCrnNo();
|
if (crnNo != null && crnNo > 0) {
|
eligible.add("CRN-" + crnNo);
|
} else {
|
try {
|
String locNo = src;
|
if (locNo != null) {
|
Integer row = com.zy.asrs.utils.Utils.getRow(locNo);
|
for (BasCrnp basCrnp : basCrnps) {
|
List<List<Integer>> controlRows = basCrnp.getControlRows$();
|
for (List<Integer> rows : controlRows) {
|
if (rows.contains(row)) {
|
eligible.add("CRN-" + basCrnp.getCrnNo());
|
break;
|
}
|
}
|
}
|
}
|
} catch (Exception ignore) {}
|
}
|
t.put("eligibleCranes", eligible);
|
t.put("conveyorPath", new ArrayList<>());
|
taskDataList.add(t);
|
}
|
|
List<WrkMast> inTasks = wrkMastService.selectList(new EntityWrapper<WrkMast>().eq("wrk_sts", WrkStsType.INBOUND_DEVICE_RUN.sts));
|
for (WrkMast wrkMast : inTasks) {
|
HashMap<String, Object> t = new HashMap<>();
|
t.put("taskId", wrkMast.getWrkNo());
|
t.put("taskType", "IN");
|
t.put("priority", wrkMast.getIoPri() == null ? 5 : wrkMast.getIoPri().intValue());
|
if (wrkMast.getAppeTime() != null) {
|
t.put("createTime", wrkMast.getAppeTime().getTime());
|
}
|
Integer targetStationId = wrkMast.getStaNo();
|
HashMap<String, Object> fromPos = new HashMap<>();
|
String matchedCrnCode = null;
|
boolean stationReady = false;
|
if (targetStationId != null) {
|
StationObjModel stationObjModel = stationIndex.get(targetStationId);
|
if (stationObjModel != null) {
|
StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, stationObjModel.getDeviceNo());
|
if (stationThread != null) {
|
Map<Integer, StationProtocol> statusMap = stationThread.getStatusMap();
|
StationProtocol sp = statusMap == null ? null : statusMap.get(targetStationId);
|
if (sp != null && sp.isAutoing() && sp.isLoading() && sp.getTaskNo().equals(wrkMast.getWrkNo())) {
|
stationReady = true;
|
matchedCrnCode = stationCrnCodeIndex.get(targetStationId);
|
fromPos.put("x", stationObjModel.getDeviceBay() * bayWidth);
|
fromPos.put("y", stationObjModel.getDeviceLev() * levHeight);
|
}
|
}
|
}
|
}
|
if (!stationReady) {
|
continue;
|
}
|
t.put("fromPos", fromPos);
|
HashMap<String, Object> toPos = new HashMap<>();
|
String dst = wrkMast.getLocNo();
|
LocMast dstLoc = dst == null ? null : locMastService.queryByLoc(dst);
|
int toX = dstLoc == null ? 0 : (dstLoc.getBay1() == null ? 0 : dstLoc.getBay1());
|
int toY = dstLoc == null ? 0 : (dstLoc.getLev1() == null ? 0 : dstLoc.getLev1());
|
toPos.put("x", toX * bayWidth);
|
toPos.put("y", toY * levHeight);
|
t.put("toPos", toPos);
|
ArrayList<String> eligible = new ArrayList<>();
|
if (matchedCrnCode != null) {
|
eligible.add(matchedCrnCode);
|
}
|
t.put("eligibleCranes", eligible);
|
t.put("conveyorPath", new ArrayList<>());
|
taskDataList.add(t);
|
}
|
|
//获取输送线任务数量
|
int currentStationTaskCount = stationOperateProcessUtils.getCurrentStationTaskCount();
|
|
HashMap<String, Object> config = new HashMap<>();
|
config.put("outMinGapSec", getIntConfig("plannerOutMinGapSec", 0));
|
config.put("wMakespan", getIntConfig("plannerWeightMakespan", 1000));
|
config.put("wPriorityEarlyFinish", getIntConfig("plannerWeightPriorityEarlyFinish", 5));
|
config.put("wWaitTime", getIntConfig("plannerWeightWaitTime", 1));
|
config.put("wOutTardiness", getIntConfig("plannerOutTardiness", 0));
|
config.put("enableOutDue", getBoolConfig("plannerEnableOutDue", false));
|
config.put("maxSolveSeconds", getIntConfig("plannerMaxSolveSeconds", 3));
|
config.put("numSearchWorkers", getIntConfig("plannerNumSearchWorkers", 8));
|
config.put("conveyorStationTaskLimit", getIntConfig("conveyorStationTaskLimit", 30));
|
config.put("conveyorStationRunningTasks", currentStationTaskCount);
|
|
HashMap<String, Object> request = new HashMap<>();
|
request.put("cranes", crnDataList);
|
request.put("tasks", taskDataList);
|
request.put("config", config);
|
|
JSONObject result;
|
boolean useInternal = getBoolConfig("plannerUseInternalSolver", true);
|
if (useInternal) {
|
try {
|
result = plannerOrtoolsSolverService.solve(JSONObject.parseObject(JSON.toJSONString(request)));
|
} catch (Throwable e) {
|
News.error("内部求解器执行失败:{}", e.getMessage());
|
result = null;
|
}
|
} else {
|
Config uriCfg = configService.selectOne(new EntityWrapper<Config>().eq("code", "plannerSolverUri"));
|
Config pathCfg = configService.selectOne(new EntityWrapper<Config>().eq("code", "plannerSolverPath"));
|
if (uriCfg == null || pathCfg == null || uriCfg.getValue() == null || pathCfg.getValue() == null) {
|
// 如果是服务调用,这里无法直接返回R.error,返回null或者empty json
|
return null;
|
}
|
String solverUri = uriCfg.getValue();
|
String solverPath = pathCfg.getValue();
|
String response;
|
try {
|
HttpHandler http = new HttpHandler.Builder()
|
.setUri(solverUri)
|
.setPath(solverPath)
|
.setTimeout(30, TimeUnit.SECONDS)
|
.setJson(JSON.toJSONString(request))
|
.build();
|
response = http.doPost();
|
} catch (Exception e) {
|
News.error("求解器调用失败:{}", e.getMessage());
|
return null;
|
}
|
try {
|
result = JSON.parseObject(response);
|
} catch (Exception ignore) {
|
return null;
|
}
|
}
|
|
try {
|
try {
|
int nowSec = (int) (System.currentTimeMillis() / 1000);
|
if (result != null && result.containsKey("schedule")) {
|
List<Object> schedule = result.getJSONArray("schedule");
|
if (schedule != null) {
|
Map<String, List<String>> groupByCrane = new HashMap<>();
|
for (Object obj : schedule) {
|
JSONObject item = (JSONObject) JSONObject.toJSON(obj);
|
String craneCode = item.getString("craneCode");
|
Integer taskId = item.getInteger("taskId");
|
String taskType = item.getString("taskType");
|
Integer startSec = item.getInteger("startSec");
|
Integer endSec = item.getInteger("endSec");
|
Integer outEtaSec = item.getInteger("outEtaSec");
|
if (craneCode == null || taskId == null || startSec == null) {
|
continue;
|
}
|
JSONObject store = new JSONObject();
|
store.put("taskId", taskId);
|
store.put("taskType", taskType);
|
store.put("startEpochSec", nowSec + startSec);
|
if (endSec != null) {
|
store.put("endEpochSec", nowSec + endSec);
|
}
|
if (outEtaSec != null) {
|
store.put("outEtaEpochSec", nowSec + outEtaSec);
|
}
|
groupByCrane.computeIfAbsent(craneCode, k -> new ArrayList<>()).add(store.toJSONString());
|
}
|
for (Map.Entry<String, List<String>> e : groupByCrane.entrySet()) {
|
String key = RedisKeyType.PLANNER_SCHEDULE.key + e.getKey();
|
redisUtil.del(key);
|
for (String s : e.getValue()) {
|
redisUtil.lSet(key, s);
|
}
|
}
|
}
|
}
|
} catch (Exception ignore) {}
|
return result;
|
} catch (Exception ignore) {
|
return null;
|
}
|
}
|
|
private Double getDoubleConfig(String code, Double def) {
|
try {
|
Config c = configService.selectOne(new EntityWrapper<Config>().eq("code", code));
|
if (c != null && c.getValue() != null && c.getValue().trim().length() > 0) {
|
return Double.parseDouble(c.getValue().trim());
|
}
|
} catch (Exception ignore) {}
|
return def;
|
}
|
|
private Integer getIntConfig(String code, Integer def) {
|
try {
|
Config c = configService.selectOne(new EntityWrapper<Config>().eq("code", code));
|
if (c != null && c.getValue() != null && c.getValue().trim().length() > 0) {
|
String v = c.getValue().trim();
|
if (v.endsWith("%")) v = v.substring(0, v.length() - 1);
|
return Integer.parseInt(v);
|
}
|
} catch (Exception ignore) {}
|
return def;
|
}
|
|
private Boolean getBoolConfig(String code, Boolean def) {
|
try {
|
Config c = configService.selectOne(new EntityWrapper<Config>().eq("code", code));
|
if (c != null && c.getValue() != null) {
|
String v = c.getValue().trim().toUpperCase();
|
if ("Y".equals(v) || "TRUE".equals(v)) return true;
|
if ("N".equals(v) || "FALSE".equals(v)) return false;
|
}
|
} catch (Exception ignore) {}
|
return def;
|
}
|
}
|