【实训消息修改为事件触发、增加指标计算流程事件】

This commit is contained in:
weizhihong 2022-09-05 16:53:28 +08:00
parent 786e0e58a9
commit 6a76691b2c
10 changed files with 326 additions and 37 deletions

View File

@ -10,11 +10,13 @@ import club.joylink.rtss.simulation.cbtc.GroupSimulationService;
import club.joylink.rtss.simulation.cbtc.Simulation; import club.joylink.rtss.simulation.cbtc.Simulation;
import club.joylink.rtss.simulation.cbtc.SimulationLifeCycleService; import club.joylink.rtss.simulation.cbtc.SimulationLifeCycleService;
import club.joylink.rtss.simulation.cbtc.event.SimulationOperationEvent; import club.joylink.rtss.simulation.cbtc.event.SimulationOperationEvent;
import club.joylink.rtss.simulation.cbtc.event.training2.*;
import club.joylink.rtss.simulation.cbtc.exception.SimulationException; import club.joylink.rtss.simulation.cbtc.exception.SimulationException;
import club.joylink.rtss.simulation.cbtc.exception.SimulationExceptionType; import club.joylink.rtss.simulation.cbtc.exception.SimulationExceptionType;
import club.joylink.rtss.simulation.cbtc.member.MemberManager; import club.joylink.rtss.simulation.cbtc.member.MemberManager;
import club.joylink.rtss.simulation.cbtc.member.SimulationMember; import club.joylink.rtss.simulation.cbtc.member.SimulationMember;
import club.joylink.rtss.simulation.cbtc.script.ScriptBO; import club.joylink.rtss.simulation.cbtc.script.ScriptBO;
import club.joylink.rtss.simulation.cbtc.training2.Index;
import club.joylink.rtss.simulation.cbtc.training2.Operation2; import club.joylink.rtss.simulation.cbtc.training2.Operation2;
import club.joylink.rtss.simulation.cbtc.training2.Step2; import club.joylink.rtss.simulation.cbtc.training2.Step2;
import club.joylink.rtss.simulation.cbtc.training2.Training2; import club.joylink.rtss.simulation.cbtc.training2.Training2;
@ -26,12 +28,15 @@ import club.joylink.rtss.vo.client.WebSocketMessageType;
import club.joylink.rtss.vo.client.factory.SocketMessageFactory; import club.joylink.rtss.vo.client.factory.SocketMessageFactory;
import club.joylink.rtss.websocket.StompMessageService; import club.joylink.rtss.websocket.StompMessageService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.event.EventListener; import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.Semaphore; import java.util.concurrent.Semaphore;
@ -41,6 +46,7 @@ import java.util.function.Consumer;
public class Training2Service { public class Training2Service {
public static final String EXECUTE_JOB_NAME = "Training2"; public static final String EXECUTE_JOB_NAME = "Training2";
public static final String SCORING_JOB_NAME = "Training2Scoring"; public static final String SCORING_JOB_NAME = "Training2Scoring";
public static final String TRAINING_INDEX_JOB_NAME = "Training2IndexStatistic";
public static final int RATE = 1000; public static final int RATE = 1000;
@Autowired @Autowired
@ -67,6 +73,9 @@ public class Training2Service {
@Autowired @Autowired
private StompMessageService stompMessageService; private StompMessageService stompMessageService;
@Autowired
private ApplicationContext applicationContext;
/** /**
* 完成步骤接口信号量 * 完成步骤接口信号量
*/ */
@ -77,6 +86,9 @@ public class Training2Service {
*/ */
private Semaphore completeOperationSemaphore = new Semaphore(1); private Semaphore completeOperationSemaphore = new Semaphore(1);
/**
* 实训运行
*/
public void run(Simulation simulation) { public void run(Simulation simulation) {
Training2 training2 = simulation.getTraining2(); Training2 training2 = simulation.getTraining2();
if (training2 == null || !training2.isStarted() || training2.isFinish()) { if (training2 == null || !training2.isStarted() || training2.isFinish()) {
@ -104,6 +116,19 @@ public class Training2Service {
tryCompleteOperation(simulation, step, operation2); tryCompleteOperation(simulation, step, operation2);
} }
/**
* 统计运行
*/
public void statisticRun(Simulation simulation) {
Training2 training2 = simulation.getTraining2();
if (training2 == null || training2.isStop()) {
return;
}
List<Index> indexList = training2.getIndexList();
if (!CollectionUtils.isEmpty(indexList)) {
indexList.stream().filter(Index::isContinuity).forEach(index -> index.getType().accept(simulation, index.getDataMap()));
}
}
/** /**
* 预览实训草稿 * 预览实训草稿
@ -190,11 +215,8 @@ public class Training2Service {
groupSimulationService.loadScenes(simulation.getId(), training2.getBgSceneJson()); groupSimulationService.loadScenes(simulation.getId(), training2.getBgSceneJson());
} }
} }
// 增加任务 // 增加实训任务
if (simulation.getJobMap().containsKey(EXECUTE_JOB_NAME)) { addTrainingJob(simulation);
simulation.removeJob(EXECUTE_JOB_NAME);
}
simulation.addJobIfAbsent(EXECUTE_JOB_NAME, () -> this.run(simulation), RATE);
training2.start(mode); training2.start(mode);
simulationLifeCycleService.resume(simulation); simulationLifeCycleService.resume(simulation);
} }
@ -215,8 +237,7 @@ public class Training2Service {
// 创建者退出则清理实训 // 创建者退出则清理实训
if (simulation.getCreator().getId().equals(user.getId())) { if (simulation.getCreator().getId().equals(user.getId())) {
training2.finish(); training2.finish();
simulation.removeJob(SCORING_JOB_NAME); //移除打分监视任务 removeTrainingJob(simulation);
simulation.removeJob(EXECUTE_JOB_NAME); //移除实训执行任务
} }
scoreMap = training2.mark(); scoreMap = training2.mark();
} else { } else {
@ -324,7 +345,10 @@ public class Training2Service {
public void handle(SimulationOperationEvent event) { public void handle(SimulationOperationEvent event) {
Simulation simulation = event.getSimulation(); Simulation simulation = event.getSimulation();
Training2 training2 = simulation.getTraining2(); Training2 training2 = simulation.getTraining2();
if (training2 != null && Objects.equals(event.getSuccessful(), true)) { if (training2 == null || training2.isStop()) {
return;
}
if (Objects.equals(event.getSuccessful(), true)) {
// 获取运行步骤 // 获取运行步骤
Step2 step = training2.getCurrentRunStep(); Step2 step = training2.getCurrentRunStep();
// 没找到返回 // 没找到返回
@ -350,10 +374,13 @@ public class Training2Service {
operation2.getCount().incrementAndGet(); operation2.getCount().incrementAndGet();
// 错误提示 // 错误提示
// 发送操作完成信息 // 发送操作完成信息
sendOperationError(operation2, simulation); applicationContext.publishEvent(new SimulationOperationErrorEvent(this, simulation, operation2));
} }
// 操作完成后,如果是测验模式则仿真启动 // 操作完成后,如果是测验模式则仿真启动
pauseOrStartSimulation(simulation, false); pauseOrStartSimulation(simulation, false);
// 指标计算
applicationContext.publishEvent(new SimulationTrainingIndexEvent(this, simulation, training2
, simOperation2.getOperationType()));
} }
} }
} }
@ -361,19 +388,25 @@ public class Training2Service {
/** /**
* 步骤提示 * 步骤提示
*/ */
public void sendStepTips(Step2 step, Simulation simulation) { @EventListener
SocketMessageVO<String> message = public void handle(SimulationStepTipEvent event) {
SocketMessageFactory.build(WebSocketMessageType.Simulation_Training_Step_Tip, simulation.getId(), step.getDescription()); Simulation simulation = event.getSimulation();
Step2 step = event.getStep();
SocketMessageVO<String> message = SocketMessageFactory.build(WebSocketMessageType.Simulation_Training_Step_Tip
, simulation.getId(), step.getDescription());
stompMessageService.sendToUser(simulation.getSimulationUserIds(), message); stompMessageService.sendToUser(simulation.getSimulationUserIds(), message);
} }
/** /**
* 步骤完成 * 步骤完成
*/ */
public void sendStepFinish(Step2 step, Simulation simulation) { @EventListener
public void handle(SimulationStepFinishEvent event) {
Simulation simulation = event.getSimulation();
Step2 step = event.getStep();
if (Step2.StepStatus.isCompletion(step.getStepStatus())) { if (Step2.StepStatus.isCompletion(step.getStepStatus())) {
SocketMessageVO<String> message = SocketMessageVO<String> message = SocketMessageFactory.build(WebSocketMessageType.Simulation_Training_Step_Finish
SocketMessageFactory.build(WebSocketMessageType.Simulation_Training_Step_Finish, simulation.getId(), "步骤完成"); , simulation.getId(), "步骤完成");
stompMessageService.sendToUser(simulation.getSimulationUserIds(), message); stompMessageService.sendToUser(simulation.getSimulationUserIds(), message);
} }
} }
@ -381,16 +414,21 @@ public class Training2Service {
/** /**
* 操作错误 * 操作错误
*/ */
public void sendOperationError(Operation2 operation2, Simulation simulation) { @EventListener
SocketMessageVO<String> message = public void handle(SimulationOperationErrorEvent event) {
SocketMessageFactory.build(WebSocketMessageType.Simulation_Training_Operate_Error, simulation.getId(), "操作错误"); Simulation simulation = event.getSimulation();
SocketMessageVO<String> message = SocketMessageFactory.build(WebSocketMessageType.Simulation_Training_Operate_Error
, simulation.getId(), "操作错误");
stompMessageService.sendToUser(simulation.getSimulationUserIds(), message); stompMessageService.sendToUser(simulation.getSimulationUserIds(), message);
} }
/** /**
* 操作完成 * 操作完成
*/ */
public void sendOperationFinish(Operation2 operation2, Simulation simulation) { @EventListener
public void handle(SimulationOperationFinishEvent event) {
Simulation simulation = event.getSimulation();
Operation2 operation2 = event.getOperation();
if (Step2.StepStatus.isCompletion(operation2.getStatus())) { if (Step2.StepStatus.isCompletion(operation2.getStatus())) {
SocketMessageVO<String> message = SocketMessageVO<String> message =
SocketMessageFactory.build(WebSocketMessageType.Simulation_Training_Operate_Finish, simulation.getId(), "操作完成"); SocketMessageFactory.build(WebSocketMessageType.Simulation_Training_Operate_Finish, simulation.getId(), "操作完成");
@ -401,7 +439,10 @@ public class Training2Service {
/** /**
* 实训完成发送消息 * 实训完成发送消息
*/ */
public void sendTrainingFinish(Training2 training2, Simulation simulation) { @EventListener
public void handle(SimulationTrainingFinishEvent event) {
Simulation simulation = event.getSimulation();
Training2 training2 = event.getTraining();
if (training2.isFinish()) { if (training2.isFinish()) {
SocketMessageVO<String> message = SocketMessageVO<String> message =
SocketMessageFactory.build(WebSocketMessageType.Simulation_Training_Finish, simulation.getId(), "实训完成"); SocketMessageFactory.build(WebSocketMessageType.Simulation_Training_Finish, simulation.getId(), "实训完成");
@ -409,6 +450,21 @@ public class Training2Service {
} }
} }
/**
* 计算实训指标消息
*/
@EventListener
public void handle(SimulationTrainingIndexEvent event) {
Simulation simulation = event.getSimulation();
Training2 training2 = event.getTraining();
// 仿真操作触发指标计算
if (!CollectionUtils.isEmpty(training2.getIndexList())) {
training2.getIndexList().stream().filter(index -> !index.isContinuity() && Objects.equals(
event.getOperationType().name(), index.getDataMap().get("operationType")))
.forEach(index -> index.getType().accept(simulation, index.getDataMap()));
}
}
/** /**
* 绘制草稿时加载实训背景 * 绘制草稿时加载实训背景
* TODO 后续看根据需求是否加载到最后一步 * TODO 后续看根据需求是否加载到最后一步
@ -483,10 +539,13 @@ public class Training2Service {
/** /**
* 判断实训步骤是否完成 * 判断实训步骤是否完成
*/ */
private void checkTrainStepCompletion(Step2 step, Simulation simulation) { private boolean checkTrainStepCompletion(Step2 step, Simulation simulation) {
step.doCompletionVail(); boolean result = step.doCompletionVail();
// 发送步骤完成信息 if (result) {
sendStepFinish(step, simulation); // 发送步骤完成信息
applicationContext.publishEvent(new SimulationStepFinishEvent(this, simulation, step));
}
return result;
} }
/** /**
@ -498,7 +557,7 @@ public class Training2Service {
if (nextStep == null) { // 步骤已经运行完毕 if (nextStep == null) { // 步骤已经运行完毕
training2.finish(); training2.finish();
// 发送实训完成消息 // 发送实训完成消息
sendTrainingFinish(training2, simulation); applicationContext.publishEvent(new SimulationTrainingFinishEvent(this, simulation, training2));
return null; return null;
} }
return nextStep; return nextStep;
@ -516,7 +575,7 @@ public class Training2Service {
// 发送步骤提示信息 // 发送步骤提示信息
if (!step.isPrompt()) { if (!step.isPrompt()) {
step.setPrompt(true); // 标识已发送过消息 step.setPrompt(true); // 标识已发送过消息
sendStepTips(step, simulation); applicationContext.publishEvent(new SimulationStepTipEvent(this, simulation, step));
} }
return true; return true;
} }
@ -573,7 +632,7 @@ public class Training2Service {
if (Step2.StepStatus.isRunning(operation2.getStatus())) { if (Step2.StepStatus.isRunning(operation2.getStatus())) {
boolean completion = operation2.doCompletion(); boolean completion = operation2.doCompletion();
// 发送操作完成信息 // 发送操作完成信息
sendOperationFinish(operation2, simulation); applicationContext.publishEvent(new SimulationOperationFinishEvent(this, simulation, operation2));
// 如果是最后一步直接检查步骤有没有完成 // 如果是最后一步直接检查步骤有没有完成
if (step.getOperations().indexOf(operation2) == (step.getOperations().size() - 1) && completion) { if (step.getOperations().indexOf(operation2) == (step.getOperations().size() - 1) && completion) {
checkStepCompletionAndSendNext(step, simulation); checkStepCompletionAndSendNext(step, simulation);
@ -585,10 +644,8 @@ public class Training2Service {
* 直接完成并尝试触发下一步 * 直接完成并尝试触发下一步
*/ */
private void checkStepCompletionAndSendNext(Step2 step, Simulation simulation) { private void checkStepCompletionAndSendNext(Step2 step, Simulation simulation) {
boolean result = step.doCompletionVail(); boolean result = checkTrainStepCompletion(step, simulation);
if (result) { if (result) {
// 发送步骤完成信息
sendStepFinish(step, simulation);
// 获取运行步骤 // 获取运行步骤
Step2 nextStep = getNextStepAndCheckTrainingStatus(simulation.getTraining2(), simulation); Step2 nextStep = getNextStepAndCheckTrainingStatus(simulation.getTraining2(), simulation);
if (nextStep == null) { if (nextStep == null) {
@ -617,6 +674,30 @@ public class Training2Service {
} }
} }
/**
* 移除实训任务
*/
private void removeTrainingJob(Simulation simulation) {
if (simulation.getJobMap().containsKey(SCORING_JOB_NAME)) {
simulation.removeJob(SCORING_JOB_NAME); //移除打分监视任务
}
if (simulation.getJobMap().containsKey(EXECUTE_JOB_NAME)) {
simulation.removeJob(EXECUTE_JOB_NAME); //移除实训执行任务
}
if (simulation.getJobMap().containsKey(TRAINING_INDEX_JOB_NAME)) {
simulation.removeJob(TRAINING_INDEX_JOB_NAME); // 移除指标记录任务
}
}
/**
* 增加实训任务
*/
private void addTrainingJob(Simulation simulation) {
removeTrainingJob(simulation);
simulation.addJobIfAbsent(EXECUTE_JOB_NAME, () -> this.run(simulation), RATE);
simulation.addJobIfAbsent(TRAINING_INDEX_JOB_NAME, () -> this.statisticRun(simulation), RATE);
}
/** /**
* 检验实训数据是否合规 * 检验实训数据是否合规
*/ */

View File

@ -0,0 +1,17 @@
package club.joylink.rtss.simulation.cbtc.event.training2;
import club.joylink.rtss.simulation.cbtc.Simulation;
import club.joylink.rtss.simulation.cbtc.event.AbstractSimulationEvent;
import club.joylink.rtss.simulation.cbtc.training2.Operation2;
import lombok.Getter;
@Getter
public class SimulationOperationErrorEvent extends AbstractSimulationEvent {
private Operation2 operation;
public SimulationOperationErrorEvent(Object source, Simulation simulation, Operation2 operation) {
super(source, simulation);
this.operation = operation;
}
}

View File

@ -0,0 +1,17 @@
package club.joylink.rtss.simulation.cbtc.event.training2;
import club.joylink.rtss.simulation.cbtc.Simulation;
import club.joylink.rtss.simulation.cbtc.event.AbstractSimulationEvent;
import club.joylink.rtss.simulation.cbtc.training2.Operation2;
import lombok.Getter;
@Getter
public class SimulationOperationFinishEvent extends AbstractSimulationEvent {
private Operation2 operation;
public SimulationOperationFinishEvent(Object source, Simulation simulation, Operation2 operation) {
super(source, simulation);
this.operation = operation;
}
}

View File

@ -0,0 +1,17 @@
package club.joylink.rtss.simulation.cbtc.event.training2;
import club.joylink.rtss.simulation.cbtc.Simulation;
import club.joylink.rtss.simulation.cbtc.event.AbstractSimulationEvent;
import club.joylink.rtss.simulation.cbtc.training2.Step2;
import lombok.Getter;
@Getter
public class SimulationStepFinishEvent extends AbstractSimulationEvent {
private Step2 step;
public SimulationStepFinishEvent(Object source, Simulation simulation, Step2 step) {
super(source, simulation);
this.step = step;
}
}

View File

@ -0,0 +1,17 @@
package club.joylink.rtss.simulation.cbtc.event.training2;
import club.joylink.rtss.simulation.cbtc.Simulation;
import club.joylink.rtss.simulation.cbtc.event.AbstractSimulationEvent;
import club.joylink.rtss.simulation.cbtc.training2.Step2;
import lombok.Getter;
@Getter
public class SimulationStepTipEvent extends AbstractSimulationEvent {
private Step2 step;
public SimulationStepTipEvent(Object source, Simulation simulation, Step2 step) {
super(source, simulation);
this.step = step;
}
}

View File

@ -0,0 +1,17 @@
package club.joylink.rtss.simulation.cbtc.event.training2;
import club.joylink.rtss.simulation.cbtc.Simulation;
import club.joylink.rtss.simulation.cbtc.event.AbstractSimulationEvent;
import club.joylink.rtss.simulation.cbtc.training2.Training2;
import lombok.Getter;
@Getter
public class SimulationTrainingFinishEvent extends AbstractSimulationEvent {
private Training2 training;
public SimulationTrainingFinishEvent(Object source, Simulation simulation, Training2 training) {
super(source, simulation);
this.training = training;
}
}

View File

@ -0,0 +1,21 @@
package club.joylink.rtss.simulation.cbtc.event.training2;
import club.joylink.rtss.simulation.cbtc.ATS.operation.Operation;
import club.joylink.rtss.simulation.cbtc.Simulation;
import club.joylink.rtss.simulation.cbtc.event.AbstractSimulationEvent;
import club.joylink.rtss.simulation.cbtc.training2.Training2;
import lombok.Getter;
@Getter
public class SimulationTrainingIndexEvent extends AbstractSimulationEvent {
private Training2 training;
private Operation.Type operationType;
public SimulationTrainingIndexEvent(Object source, Simulation simulation, Training2 training, Operation.Type operationType) {
super(source, simulation);
this.operationType = operationType;
this.training = training;
}
}

View File

@ -1,10 +1,13 @@
package club.joylink.rtss.simulation.cbtc.training2; package club.joylink.rtss.simulation.cbtc.training2;
import club.joylink.rtss.simulation.cbtc.Simulation; import club.joylink.rtss.simulation.cbtc.Simulation;
import lombok.Getter;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
@Getter
public class Index { public class Index {
private int id; private int id;
@ -12,10 +15,34 @@ public class Index {
private Map<String, Object> dataMap; private Map<String, Object> dataMap;
/**
* 指标名称
*/
private String name;
/**
* 指标描述
*/
private String description;
/**
* 统计频率
*/
private CalculationRate rate;
/**
* 连续性指标
*/
public boolean isContinuity() {
return Objects.equals(CalculationRate.CONTINUITY, this.rate);
}
public enum Type implements BiConsumer<Simulation, Map<String, Object>> { public enum Type implements BiConsumer<Simulation, Map<String, Object>> {
T1{ T1 {
@Override @Override
public void accept(Simulation simulation, Map<String, Object> dataMap) { public void accept(Simulation simulation, Map<String, Object> dataMap) {
IndexValue indexValue = new IndexValue(simulation, this);
} }
}, },
@ -26,4 +53,15 @@ public class Index {
} }
}, },
} }
/**
* 计算频率
*/
public enum CalculationRate {
// 触发式
TRIGGERED,
// 连续性计算
CONTINUITY;
}
} }

View File

@ -0,0 +1,40 @@
package club.joylink.rtss.simulation.cbtc.training2;
import club.joylink.rtss.simulation.cbtc.Simulation;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.Map;
/**
* 指标值
*/
@Data
@NoArgsConstructor
public class IndexValue {
/**
* 指标类型
*/
private Index.Type type;
/**
* 记录指标对象信息
*/
private Map<String, Object> params;
/**
* 指标值
*/
private Object value;
/**
* 统计时间
*/
private LocalDateTime time;
public IndexValue(Simulation simulation, Index.Type type) {
this.type = type;
this.time = simulation.getCorrectSystemTime();
}
}

View File

@ -58,6 +58,11 @@ public class Training2 {
private LocalDateTime updateTime; private LocalDateTime updateTime;
/**
* 实训监控指标
*/
private List<Index> indexList;
/* -------------------------------------- 运行时参数 -------------------------------------- */ /* -------------------------------------- 运行时参数 -------------------------------------- */
/** /**
@ -85,6 +90,11 @@ public class Training2 {
*/ */
private ScriptBO.Mode mode; private ScriptBO.Mode mode;
/**
* 指标值信息
*/
private Map<Index.Type, List<IndexValue>> indexValueMap;
public Training2(DraftTraining2WithBLOBs draftTraining2, Simulation simulation) { public Training2(DraftTraining2WithBLOBs draftTraining2, Simulation simulation) {
this.id = draftTraining2.getId(); this.id = draftTraining2.getId();
this.mapId = draftTraining2.getMapId(); this.mapId = draftTraining2.getMapId();
@ -122,9 +132,7 @@ public class Training2 {
this.started = true; this.started = true;
this.mode = mode; this.mode = mode;
this.finish = false; this.finish = false;
if (this.steps != null) { resetStepAndIndex();
this.steps.forEach(Step2::reset);
}
} }
public boolean finish() { public boolean finish() {
@ -136,9 +144,7 @@ public class Training2 {
public void reset() { public void reset() {
this.started = false; this.started = false;
this.finish = false; this.finish = false;
if (this.steps != null) { resetStepAndIndex();
this.steps.forEach(Step2::reset);
}
} }
public Operation2 getOperation(int id) { public Operation2 getOperation(int id) {
@ -194,6 +200,24 @@ public class Training2 {
return step; return step;
} }
/**
* 是否已经停止
*/
public boolean isStop() {
return !this.started || this.finish;
}
private void resetStepAndIndex() {
if (!CollectionUtils.isEmpty(this.indexList)) {
this.indexValueMap = new HashMap<>(this.indexList.size());
} else {
this.indexValueMap = new HashMap<>();
}
if (this.steps != null) {
this.steps.forEach(Step2::reset);
}
}
public enum Type { public enum Type {
SINGLE, SINGLE,
SCENE, SCENE,