Commit 78d8bf59 authored by Tong Li's avatar Tong Li

倒排

parent 78d5f5e4
package com.aps.entity.Algorithm;
/**
* 作者:佟礼
* 时间:2026-05-27
*/
public class DraftScheduleResult {
private int operationId;
private int idealStartTime;
private int idealEndTime;
private Long selectedMachineId;
private double processTime;
private int groupId;
private int sequence;
public DraftScheduleResult(int operationId, int idealStartTime, int idealEndTime,
Long selectedMachineId, double processTime, int groupId, int sequence) {
this.operationId = operationId;
this.idealStartTime = idealStartTime;
this.idealEndTime = idealEndTime;
this.selectedMachineId = selectedMachineId;
this.processTime = processTime;
this.groupId = groupId;
this.sequence = sequence;
}
public int getOperationId() { return operationId; }
public int getIdealStartTime() { return idealStartTime; }
public int getIdealEndTime() { return idealEndTime; }
public Long getSelectedMachineId() { return selectedMachineId; }
public double getProcessTime() { return processTime; }
public int getGroupId() { return groupId; }
public int getSequence() { return sequence; }
}
......@@ -175,6 +175,12 @@ private Long isInterrupt = 1l;
private int anchorTimeSecond = 0;
private transient boolean backwardFailed = false;
private transient boolean scheduled = false;
private transient Integer actualFinishTime;
public enum SchedulingMode {
FORWARD, BACKWARD
}
......
......@@ -67,6 +67,16 @@ public class Material {
*/
private Long CkeckLeadTime;
/**
* 最小生产量
*/
private BigDecimal minProduction;
/**
* 最大生产量
*/
private BigDecimal maxProduction;
@Override
public String toString() {
return "Material{" +
......
......@@ -290,7 +290,7 @@ public class GeneticDecoder {
return 1;
}
private void CreateNewOpSequence(Chromosome chromosome)
private void CreateNewOpSequence(Chromosome chromosome,boolean isJit)
{
//成品订单
......@@ -342,10 +342,16 @@ public class GeneticDecoder {
if (entry == null) {
continue;
}
entry.setSchedulingMode(Entry.SchedulingMode.FORWARD.name());
if(isJit) {
entry.setSchedulingMode(Entry.SchedulingMode.BACKWARD.name());
}else {
entry.setSchedulingMode(Entry.SchedulingMode.FORWARD.name());
}
if(entry!=null&&entry.getDependentOnOrderIds().size()>0)
{
if(isJit) {
finalSequence.add(num);
}
for (int order : entry.getDependentOnOrderIds()) {
// 依赖的半成品订单可能不在旧排序里,缺失时跳过,避免空指针。
......@@ -357,8 +363,9 @@ public class GeneticDecoder {
InsertSimSequence(orderProcessCounter,num1,finalSequence,sfSequence,allOperations);
}
}
finalSequence.add(num);
if(!isJit) {
finalSequence.add(num);
}
}else {
finalSequence.add(num);
......@@ -503,7 +510,7 @@ public class GeneticDecoder {
if(isnew==1) {
long t2 = System.nanoTime();
CreateNewOpSequence(chromosome);
CreateNewOpSequence(chromosome,isJit);
// CreateNewOpSequence 会重建半成品插入顺序,生成后再规整一次,避免重复调度同一订单。
normalizeOperationSequencing(chromosome);
// FileHelper.writeLogFile("[PERF] serialDecode CreateNewOpSequence 耗时=" + fmtMs(System.nanoTime() - t2));
......@@ -605,15 +612,24 @@ public class GeneticDecoder {
}
}
for (Map.Entry<Integer, List<Entry>> entry : entrysBygroupId.entrySet()) {
int groupId = entry.getKey();
Entry firstOp = entry.getValue().get(0);
if (Entry.SchedulingMode.BACKWARD.name().equals(
firstOp.getSchedulingMode())) {
orderSchedulingInfo.put(groupId,
new AbstractMap.SimpleEntry<>(true, 0));
}
Set<Integer> semiFinishedOrderIds=new HashSet<>();
if(isJit)
{
// 半成品订单ID集合
semiFinishedOrderIds = chromosome.getOrders().stream()
.filter(Order::isNewSfCreate).map(Order::getId).collect(Collectors.toSet());
}
for (Map.Entry<Integer, List<Entry>> entry : entrysBygroupId.entrySet()) {
int groupId = entry.getKey();
Entry firstOp = entry.getValue().get(0);
if (Entry.SchedulingMode.BACKWARD.name().equals(
firstOp.getSchedulingMode())&&semiFinishedOrderIds.contains(groupId)) {
orderSchedulingInfo.put(groupId,
new AbstractMap.SimpleEntry<>(true, 0));
}
}
// Map<Long, String> machineState = chromosome.getMachines().stream()
......@@ -625,43 +641,82 @@ public class GeneticDecoder {
// 记录已经整单转正排的订单,避免倒排序列后续再次进入同一个 groupId。
Set<Integer> forwardFallbackOrderIds = new HashSet<>();
// 方案1a+方案2混合排程:预检并处理有半成品依赖的订单
Set<Integer> hybridProcessedOrderIds = new HashSet<>();
if (isJit && _globalParam.isIsCheckSf()) {
runHybridPreCheckAndSchedule(chromosome, machineIdMap, machineTasksCache,
entryIndexById, scheduleIndexById, opMachineKeyMap,
entrysBygroupId, orderDueDate, orderSchedulingInfo,
orderProcessCounter, orderLastEndTime, hybridProcessedOrderIds);
}
// 混合排程:半成品正排后需要重排的成品订单
Set<Integer> reForwardFinishedOrderIds = new HashSet<>();
// 延迟重排映射:半成品ID → 关联成品ID集合(等半成品全部排完后再处理)
Map<Integer, Set<Integer>> pendingReForwardMap = new HashMap<>();
Map<Integer, Order> orders = chromosome.getOrders().stream()
.collect(Collectors.toMap(
Order::getId,
// 2. Value映射:Material → Material
order -> order
));
int opCount = 0;
long slowOpThresholdNs = 500_000_000L; // 500ms
for (int groupId : chromosome.getOperationSequencing()) {
int scheduledCount = orderProcessCounter.get(groupId);
if(groupId==7)
{
int i=0;
}
List<Entry> orderOps=new ArrayList<>();
boolean orderIsJit=orderDueDate.get(groupId)>0;
AbstractMap.SimpleEntry<Boolean, Integer> schedInfo = orderSchedulingInfo.get(groupId);
orderIsJit = schedInfo != null && schedInfo.getKey();
orderIsJit = schedInfo != null && schedInfo.getKey();
// 半成品倒排失败后,关联成品强制正排
if (reForwardFinishedOrderIds.contains(groupId)) {
orderIsJit = false;
schedInfo = new AbstractMap.SimpleEntry<>(false, 0);
}
int orderAnchor = schedInfo != null ? schedInfo.getValue() : 0;
// 半成品订单首次处理时,延迟计算锚点:沿依赖链找到根成品工序并正推
if (orderIsJit && orderAnchor == 0) {
orderAnchor =bom.computeSemiFinishedAnchor(this,groupId, entrysBygroupId,
opMachineKeyMap, chromosome,
scheduleIndexById, machineTasksCache, machineIdMap, entryIndexById);
if(orderAnchor<0) {
orderIsJit=false;
orderSchedulingInfo.put(groupId,
new AbstractMap.SimpleEntry<>(false, -1));
}else {
orderSchedulingInfo.put(groupId,
new AbstractMap.SimpleEntry<>(true, orderAnchor));
if (semiFinishedOrderIds.contains(groupId)) {
Order order= orders.get(groupId);
orderAnchor =bom.computeHybridSemiAnchor(this,groupId, entrysBygroupId,
opMachineKeyMap, chromosome, scheduleIndexById,
machineTasksCache, machineIdMap, entryIndexById,
reForwardFinishedOrderIds,_globalParam,order);
if (orderAnchor < 0) {
orderIsJit = false;
orderSchedulingInfo.put(groupId,
new AbstractMap.SimpleEntry<>(false, -1));
if (!reForwardFinishedOrderIds.isEmpty()) {
pendingReForwardMap.put(groupId,
new HashSet<>(reForwardFinishedOrderIds));
reForwardFinishedOrderIds.clear();
}
} else {
orderSchedulingInfo.put(groupId,
new AbstractMap.SimpleEntry<>(true, orderAnchor));
}
} else {
orderAnchor = bom.computeSemiFinishedAnchor(this, groupId, entrysBygroupId,
opMachineKeyMap, chromosome,
scheduleIndexById, machineTasksCache, machineIdMap, entryIndexById);
if (orderAnchor < 0) {
orderIsJit = false;
orderSchedulingInfo.put(groupId,
new AbstractMap.SimpleEntry<>(false, -1));
} else {
orderSchedulingInfo.put(groupId,
new AbstractMap.SimpleEntry<>(true, orderAnchor));
}
}
}
......@@ -728,6 +783,23 @@ public class GeneticDecoder {
orderProcessCounter.put(groupId, entrysBygroupId.get(groupId).size());
orderLastEndTime.put(groupId, forwardEndTime);
forwardFallbackOrderIds.add(groupId);
if (semiFinishedOrderIds.contains(groupId)) {
Set<Integer> fallbackFinishedIds = new HashSet<>();
List<Integer> targetFinishedOpIds = currentOp.getTargetFinishedOperationId();
if (targetFinishedOpIds != null && !targetFinishedOpIds.isEmpty()) {
for (Integer targetOpId : targetFinishedOpIds) {
Entry finishedOp = entryIndexById.get(targetOpId);
if (finishedOp != null) {
fallbackFinishedIds.add(finishedOp.getGroupId());
}
}
}
reForwardFinishedOrders(groupId, fallbackFinishedIds,
chromosome, machineTasksCache, scheduleIndexById,
machineIdMap, entryIndexById, opMachineKeyMap,
entrysBygroupId, orderProcessCounter, orderLastEndTime,
forwardFallbackOrderIds, orderSchedulingInfo);
}
opCount += entrysBygroupId.get(groupId).size();
continue;
}
......@@ -738,6 +810,22 @@ public class GeneticDecoder {
orderProcessCounter.put(groupId, orderProcessCounter.get(groupId) + 1);
orderLastEndTime.put(groupId, actualEndTime);
if (semiFinishedOrderIds.contains(groupId) &&
orderProcessCounter.get(groupId) >= entrysBygroupId.get(groupId).size()) {
Set<Integer> pendingFinishedIds = pendingReForwardMap.remove(groupId);
if (pendingFinishedIds != null && !pendingFinishedIds.isEmpty()) {
int semiEndTime = orderLastEndTime.get(groupId);
FileHelper.writeLogFile("[ReForward] 半成品" + groupId
+ "全部排完(结束时间=" + semiEndTime + "),重新正排关联成品"
+ pendingFinishedIds);
reForwardFinishedOrders(groupId, pendingFinishedIds,
chromosome, machineTasksCache, scheduleIndexById,
machineIdMap, entryIndexById, opMachineKeyMap,
entrysBygroupId, orderProcessCounter, orderLastEndTime,
forwardFallbackOrderIds, orderSchedulingInfo);
}
}
}
if(chromosome.getReOrderids()!=null&&chromosome.getReOrderids().size()>0) {
chromosome.getOperationSequencing().removeIf(t -> chromosome.getReOrderids().contains(t));
......@@ -760,7 +848,7 @@ public class GeneticDecoder {
int isnew= generateGlobalOpList(chromosome);
if(isnew==1) {
CreateNewOpSequence(chromosome);
CreateNewOpSequence(chromosome, _globalParam.isJit());
// CreateNewOpSequence 会重建半成品插入顺序,生成后再规整一次,避免重复调度同一订单。
normalizeOperationSequencing(chromosome);
}
......@@ -1437,7 +1525,7 @@ public class GeneticDecoder {
// 下面开始生成正式的 geneDetails。
if (_globalParam.is_smoothChangeOver()) {
if (isJit && islockMachineTime) {
if (isJit && islockMachineTime&&machineTasks!=null&&machineTasks.size()>0) {
// JIT 倒排 + 换型:同时校验前置换型、当前加工、后置换型能否插入相邻任务窗口。
BackwardChangeoverHelper.JitBackwardScheduleResult backwardResult =
backwardChangeoverHelper.scheduleJitBackwardWithChangeover(machine, operation, earliestStartTime,
......@@ -1514,11 +1602,8 @@ public class GeneticDecoder {
.orElse(0);
if (startTime < 0) {
if(islockMachineTime)
{
// FileHelper.writeLogFile(" 半成品222222 " + operation.getGroupId() + " - " + operation.getSequence() + ",开始时间: " + startTime + ",结束时间: " + 0 + ",处理时间: " + processingTime + ", 后处理: " + teardownTime +
// ", 前处理: " + preTime + ", 换型: " + setupTime + ", 数量: " + operation.getQuantity() + ", 设备: " + machine.getId() + ", 是否可中断: " + operation.getIsInterrupt());
if (isJit && islockMachineTime) {
return OperationScheduleResult.forwardFallback();
}
return OperationScheduleResult.success(null);
......@@ -3127,5 +3212,41 @@ if(geneDetails!=null&&geneDetails.size()>0)
orders.removeIf(t ->newoorderids.contains(t.getId()));
}
}
private void reForwardFinishedOrders(int semiOrderId,
Set<Integer> reForwardFinishedOrderIds,
Chromosome chromosome,
Map<Long, CopyOnWriteArrayList<GAScheduleResult>> machineTasksCache,
Map<Integer, GAScheduleResult> scheduleIndexById,
Map<Long, Machine> machineIdMap,
Map<Integer, Entry> entryIndexById,
Map<String, OpMachine> opMachineKeyMap,
Map<Integer, List<Entry>> entrysBygroupId,
Map<Integer, Integer> orderProcessCounter,
Map<Integer, Integer> orderLastEndTime,
Set<Integer> forwardFallbackOrderIds,
Map<Integer, AbstractMap.SimpleEntry<Boolean, Integer>> orderSchedulingInfo) {
for (int finishedId : reForwardFinishedOrderIds) {
int scheduledCount = orderProcessCounter.getOrDefault(finishedId, 0);
if (scheduledCount == 0) {
continue;
}
clearOrderSchedulingForForwardFallback(chromosome, finishedId,
machineTasksCache, scheduleIndexById);
int forwardEndTime = scheduleOrderForwardFallback(finishedId,
chromosome, machineIdMap, machineTasksCache,
entryIndexById, scheduleIndexById, opMachineKeyMap);
orderProcessCounter.put(finishedId,
entrysBygroupId.get(finishedId).size());
orderLastEndTime.put(finishedId, forwardEndTime);
forwardFallbackOrderIds.add(finishedId);
orderSchedulingInfo.put(finishedId,
new AbstractMap.SimpleEntry<>(false, 0));
FileHelper.writeLogFile("[ReForward] 半成品" + semiOrderId
+ "倒排早于基准时间,成品" + finishedId + "重新正排,结束时间=" + forwardEndTime);
}
reForwardFinishedOrderIds.clear();
}
}
......@@ -385,4 +385,113 @@ if(isJit)
return Math.max(estimatedStartTime, rawTime);
}
/**
* 半成品锚点计算(JIT倒排模式:成品先于半成品,直接取成品已排的开始时间)
* 返回 -1 触发正排+标记成品重排,>=0 作为半成品倒排锚点(半成品最晚完成时间)
*/
public int computeHybridSemiAnchor(GeneticDecoder decoder,int semiOrderId,
Map<Integer, List<Entry>> entrysBygroupId,
Map<String, OpMachine> opMachineKeyMap,
Chromosome chromosome,
Map<Integer, GAScheduleResult> scheduleIndexById,
Map<Long, CopyOnWriteArrayList<GAScheduleResult>> machineTasksCache,
Map<Long, Machine> machineIdMap,
Map<Integer, Entry> entryIndexById,
Set<Integer> reForwardFinishedOrderIds,GlobalParam _globalParam,Order order) {
List<Entry> sfOps = entrysBygroupId.get(semiOrderId);
if (sfOps == null || sfOps.isEmpty()) return 0;
Entry firstSfOp = sfOps.stream().min(Comparator.comparing(Entry::getSequence)).orElse(null);
if (firstSfOp == null) return 0;
List<Integer> targetFinishedOpIds = firstSfOp.getTargetFinishedOperationId();
if (targetFinishedOpIds == null || targetFinishedOpIds.isEmpty()) {
return 0;
}
// 找关联的成品订单
int finishedOrderId = -1;
Entry targetFinishedOp = null;
for (Integer targetOpId : targetFinishedOpIds) {
Entry op = entryIndexById.get(targetOpId);
if (op != null) {
targetFinishedOp = op;
finishedOrderId = op.getGroupId();
break;
}
}
if (finishedOrderId <= 0) return 0;
List<Entry> finishedOps = entrysBygroupId.get(finishedOrderId).stream()
.sorted(Comparator.comparing(Entry::getSequence))
.collect(Collectors.toList());
if (finishedOps.isEmpty()) return 0;
// JIT倒排模式成品已排完,直接从scheduleIndexById取成品第一道工序的开始时间
Entry finishedFirstOp = finishedOps.get(0);
GAScheduleResult finishedFirstResult = scheduleIndexById.get(finishedFirstOp.getId());
if (finishedFirstResult == null) {
// 成品还没排(不应该发生在JIT模式),降级为原有逻辑
return computeSemiFinishedAnchor(decoder, semiOrderId, entrysBygroupId,
opMachineKeyMap, chromosome, scheduleIndexById, machineTasksCache,
machineIdMap, entryIndexById);
}
String schedulingMode = finishedFirstOp.getSchedulingMode();
if (Entry.SchedulingMode.BACKWARD.name().equals(schedulingMode)) {
// 成品倒排成功 → 半成品倒排:锚点 = 成品开始时间 - 缓冲
int semiLatestEndTime = finishedFirstResult.getStartTime();
int semiTotalTime = 0;
for (Entry op : sfOps) {
String key = semiOrderId + "_" + op.getSequence();
OpMachine opt = opMachineKeyMap.get(key);
if (opt != null) semiTotalTime += calculateOperationProcessingTime(op, opt,_globalParam);
}
Material material= chromosome.getMaterials().get(order.getMaterialId());
int ckeckLeadTime=0;
if(material.getCkeckLeadTime()!=null)
{
ckeckLeadTime=(material.getCkeckLeadTime().intValue()*24*60*60);
}
semiTotalTime+=ckeckLeadTime;
if (semiLatestEndTime - semiTotalTime < 0) {
FileHelper.writeLogFile("[HybridSemi] 半成品" + semiOrderId +
" 倒排开工<0,正排+成品" + finishedOrderId + "重排");
reForwardFinishedOrderIds.add(finishedOrderId);
return -1;
}
FileHelper.writeLogFile("[HybridSemi] 半成品" + semiOrderId +
" 锚点=" + semiLatestEndTime + " (成品" + finishedOrderId +
"开始=" + finishedFirstResult.getStartTime() + ")");
return semiLatestEndTime-ckeckLeadTime;
} else {
// 成品正排了 → 半成品也正排
FileHelper.writeLogFile("[HybridSemi] 成品" + finishedOrderId +
"已正排,半成品" + semiOrderId + "也正排");
reForwardFinishedOrderIds.add(finishedOrderId);
return -1;
}
}
private int calculateOperationProcessingTime(Entry op, OpMachine machineOption,GlobalParam _globalParam) {
int total = 0;
if (op.getConstTime() == 1)//常数时间
{
total = (int) Math.ceil(machineOption.getRuntime().doubleValue());
} else {
double t = machineOption.getRuntime().doubleValue() / machineOption.getSingleOut().doubleValue() * (op.getQuantity());
total = (int) Math.ceil(t);
}
if (op.getSetupTime() != null) total += op.getSetupTime().intValue();
total += op.getTeardownTime();
if ( !_globalParam.is_smoothSetup()) {
total += op.getPreTime();
}
return total;
}
}
......@@ -540,13 +540,20 @@ public class MachineCalculator {
double e= (double)processingTime/slot.getEfficiency();
LocalDateTime endCandidate = startCandidate.plusSeconds((int)Math.ceil(e));
LocalDateTime endCandidate=null;
LocalDateTime startCandidate1 = null;
if(isJit)
{
endCandidate = endCandidate1;
startCandidate1=endCandidate.plusSeconds(-(int) Math.ceil(e));;
}else {
endCandidate = startCandidate.plusSeconds((int) Math.ceil(e));
startCandidate1=startCandidate;
}
ScheduleResultDetail time = new ScheduleResultDetail();
time.setKey(slot.getKey());
time.setStartTime((int) ChronoUnit.SECONDS.between(baseTime, startCandidate));
time.setStartTime((int) ChronoUnit.SECONDS.between(baseTime, startCandidate1));
time.setEndTime((int) ChronoUnit.SECONDS.between(baseTime, endCandidate));
time.setOneTime(oneTime);
time.setQuantity(quantity);
......
......@@ -1250,7 +1250,7 @@ if(demand==null)
}
}
}
FileHelper.writeLogFile("BOM:"+material.getCode());
// FileHelper.writeLogFile("BOM:"+material.getCode());
if (needed <= 0) {
orderMaterial.setYpQty(allneeded - needed);
orderMaterial.setQjQty(needed);
......@@ -1285,11 +1285,11 @@ if(demand==null)
for (RoutingSupportingReplace rsr : routingsupportingreplaces2) {
Material material1 = materials.get(rsr.getTargetmaterialid());
FileHelper.writeLogFile("RoutingSupportingReplace:"+material1.getCode());
// FileHelper.writeLogFile("RoutingSupportingReplace:"+material1.getCode());
if (material1 == null) {
break;
}
FileHelper.writeLogFile("RoutingSupportingReplace: "+(commitChanges?"1":"0")+" " +material1.getCode()+": "+useStock);
// FileHelper.writeLogFile("RoutingSupportingReplace: "+(commitChanges?"1":"0")+" " +material1.getCode()+": "+useStock);
OrderMaterialRequirement orderMaterial1 = MaterialStock(material1, rsr.getTargetmaterialid(), orderMaterial.getOrderId(), orderMaterial.getChildOrderId(), operationId, allneeded, needed, earliestStartTime, commitChanges);
if (orderMaterial1 != null) {
......@@ -1297,7 +1297,7 @@ if(demand==null)
needed -= useStock;
FileHelper.writeLogFile("RoutingSupportingReplace: "+(commitChanges?"1":"0")+" " +material1.getCode()+": "+useStock);
// FileHelper.writeLogFile("RoutingSupportingReplace: "+(commitChanges?"1":"0")+" " +material1.getCode()+": "+useStock);
orderMaterial.setUseStock(orderMaterial.getUseStock() + useStock);
orderMaterial.getReplaceMaterial().add(orderMaterial1);
if (needed <= 0) {
......
......@@ -2014,6 +2014,12 @@ public class PlanResultService {
.sorted(Comparator.comparingInt(GAScheduleResult::getOperationId))
.collect(Collectors.toList());
Map<Integer, Order> orders = schedule.getOrders().stream()
.collect(Collectors.toMap(
Order::getId,
// 2. Value映射:Material → Material
order -> order
));
for (GAScheduleResult job : sortedJobs) {
StringBuilder sb = new StringBuilder();
String TargetFinishedOperationIds="";
......@@ -2023,14 +2029,16 @@ public class PlanResultService {
.map(String::valueOf) // 把每个Integer转成String
.collect(Collectors.joining(","));
}
Order order=orders.get(job.getGroupId());
sb.append(String.format(
"[%d-%d]:[%s-%s] Order %d,OrderID %s, Machine %d, Operation %d, Quantity %.1f, processingTime %.1f, 前处理 %d, 后处理 %d, 离散参数 %d, bomtime %s,TargetOperationId %s",
"[%d-%d]:[%s-%s] Order %d,OrderID %s,OrderDueDate %s, Machine %d, Operation %d, Quantity %.1f, processingTime %.1f, 前处理 %d, 后处理 %d, 离散参数 %d, bomtime %s,TargetOperationId %s",
job.getStartTime(),
job.getEndTime(),
ConvertTime(job.getStartTime()),
ConvertTime(job.getEndTime()),
job.getGroupId(),
job.getOrderId(),
order.getDueDate().format(DateTimeFormatter.ofPattern("YYYY-MM-dd HH:mm:ss")),
job.getMachineId(),
job.getOperationId(),
job.getQuantity(),
......@@ -2414,6 +2422,9 @@ if(job.getGeneDetails()!=null)
material.setMaterialTypeName(m.getMaterialTypeName());
material.setCode(m.getCode());
material.setName(m.getName());
material.setMaxProduction(m.getMaxProduction());
material.setMinProduction(m.getMinProduction());
// material.setCkeckLeadTime(m.getInspectDuration());
// material.setPurchaseLeadTime(m.getPurchaseDuration());
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment