void ofxTSPSOscSender::update(){ if (strcmp(oldip.c_str(), ip.c_str()) != 0 || oldport != port){ oldip = ip; oldport = port; reroute(ip, port); } }
void multitrain_driver() { MultiTrainDriver me; initDriver(&me); unsigned int naggCount = 0; unsigned int updateStoppingDistanceCount = 0; for (;;) { int tid = -1; MultiTrainDriverMsg actualMsg; DriverMsg* msg = (DriverMsg*)&actualMsg; msg->data3 = 0; Receive(&tid, (char *)msg, sizeof(MultiTrainDriverMsg)); if (msg->type != REPORT_INFO && msg->type != QUERY_STOP_COUNT && msg->type != MULTI_TRAIN_DRIVER_COURIER && msg->type != SENSOR_TRIGGER && msg->type != DELTA_DISTANCE) { Reply(tid, (char*)1, 0); } switch (msg->type) { case SET_SPEED: { if (!me.tailMode) { Reply(msg->replyTid, (char*)1, 0); } groupSetSpeed(&me, msg->data2); break; } case SENSOR_TRIGGER: { if (me.tailMode && tid == me.sensorWatcher) { Reply(tid, (char *)NULL, 0); break; } if (me.tailMode) { int isHandled = 0; Send(me.trainId[0], (char*)msg, sizeof(DriverMsg), (char *)&isHandled, sizeof(int)); Reply(tid, (char *)&isHandled, sizeof(int)); } else { int isSensorReserved = QueryIsSensorReserved(&me, msg->data2, msg->data3); if (isSensorReserved) { // sensor is reserved for (int i = 0; i < me.numTrainInGroup; i ++) { int isHandled = 0; Send(me.trainId[i], (char*)msg, sizeof(DriverMsg), (char *)&isHandled, sizeof(int)); if (isHandled) { break; } } } Reply(tid, (char *)NULL, 0); } break; } // case case NAVIGATE_NAGGER: { if (me.tailMode) break; updateInfo(&me); if (me.routeRemaining != -1) { if (!me.stopCommited) { if (shouldStopNow(&me)) { if (me.route.nodes[me.stopNode].num == REVERSE) { //TrainDebug(&me, "Navi reversing."); groupSetSpeed(&me, -1); // reverse } else { //TrainDebug(&me, "Navi Nagger stopping."); groupSetSpeed(&me, 0); // stopping me.route.length = 0; // Finished the route. me.testMode = 0; me.routeRemaining = -1; } me.stopCommited = 1; me.useLastSensorNow = 0; me.stopNow = 0; me.stopSensorHit = 0; } else { if ((++updateStoppingDistanceCount & 15) == 0) updateStopNode(&me); } } } if (me.nextSetSwitchNode != -1 && (++me.setSwitchNaggerCount & 3) == 0) { trySetSwitch_and_getNextSwitch(&me); } if (me.rerouteCountdown-- == 0) { if (me.testMode) { int reserveStatus = makeReservation(&me, 440); if (reserveStatus == RESERVE_FAIL) { reroute(&me); } else { me.nextSetSwitchNode = -1; updateSetSwitch(&me); groupSetSpeed(&me, 8); } } else { // reroute if (me.route.length != 0) { setRoute(&me, &(me.info[0].pos), &(me.routeMsg)); } } } break; } case UPDATE_PREDICTION: { if (me.tailMode) { MultiTrainDriverCourierMsg cMsg; cMsg.destTid = me.headTid; cMsg.msg = actualMsg; cMsg.msg.data = MyTid(); Reply(me.courier, (char *)&cMsg, sizeof(MultiTrainDriverCourierMsg)); break; } for (int i = 0; i < MAX_TRAIN_IN_GROUP; i++) { if (actualMsg.data == me.trainId[i]) { for (int j = 0; j < actualMsg.numSensors; j++) { me.sensorToReserve[i][j] = actualMsg.sensors[j]; } me.numSensorToReserve[i] = actualMsg.numSensors; } } makeReservation(&me, me.info[0].maxStoppingDistance); break; } case STOP_COMPLETED: { // notify actual train controller. if (me.tailMode) { MultiTrainDriverCourierMsg cMsg; cMsg.destTid = me.headTid; cMsg.msg = actualMsg; cMsg.msg.data = MyTid(); Reply(me.courier, (char *)&cMsg, sizeof(MultiTrainDriverCourierMsg)); break; } me.stoppedCount++; if (me.stoppedCount == me.numTrainInGroup) { makeReservation(&me, 1); if (me.isReversing) { handleReverse(&me); } else if (me.route.length != 0) { // Reroute. setRoute(&me, &(me.info[0].pos), &(me.routeMsg)); } } if (!me.reserveTrackMode) { clearReservation(me.trackManager, me.trainNum); } break; } case SET_ROUTE: { Reply(msg->replyTid, (char*)1, 0); me.routeMsg = *msg; setRoute(&me, &(me.info[0].pos), msg); break; } case GET_POSITION: { if (me.tailMode) { PrintDebug(me.ui, "Get position from a tail train ??"); break; } // If don't have any valid info yet, reply empty message if (me.infoUpdater == -1) { Reply(msg->replyTid, (char*)1, 0); } else { Reply(msg->replyTid, (char*)&(me.info[0]), sizeof(DumbDriverInfo)); } break; } case FIND_POSITION: { if (me.tailMode) { PrintDebug(me.ui, "find position while merge????"); break; } me.reserveTrackMode = (msg->data2 == RESERVE); PrintDebug(me.ui, "Train locking %d", me.trainNum); // Only 1 train can lock at the same time. lock(me.timeserver); // begin finding position in a slow speed DumbTrainSetSpeed(me.trainId[0], 5); Reply(msg->replyTid, (char*)1, 0); for (;;) { Receive(&tid, (char*)msg, sizeof(MultiTrainDriverMsg)); Reply(tid, (char*)1, 0); if (msg->type == SENSOR_TRIGGER) { Send(me.trainId[0], (char*)msg, sizeof(DriverMsg), (char*)NULL, 0); DumbTrainSetSpeed(me.trainId[0], 0); break; } else if (msg->type == GET_POSITION) { Reply(msg->replyTid, (char*)1, 0); } else { PrintDebug(me.ui, "WARNN Drop %d", msg->type); } } DriverMsg dMsg; dMsg.type = REPORT_INFO; for (int i = 0; i < me.numTrainInGroup; i++) { Send(me.trainId[i], (char *)&dMsg, sizeof(DriverMsg), (char*)&me.info[i], sizeof(DumbDriverInfo)); } me.infoUpdater = Create(3, trainNavigateNagger); unlock(); break; } case MERGE_HEAD: { if (me.tailMode) { PrintDebug(me.ui, "Cannot be a head when in tail mode??"); break; } me.reserveTrackMode = 1; // Head always reserves track. // Other train controller's id. me.trainId[me.numTrainInGroup] = msg->data2; me.numTrainInGroup++; Reply(msg->replyTid, (char*)1, 0); DriverMsg dMsg; dMsg.type = QUERY_STOP_COUNT; int tailStopCount = 0; Send(msg->data2, (char *)&dMsg, sizeof(DriverMsg), (char *)&tailStopCount, sizeof(int)); me.stoppedCount += tailStopCount; dMsg.type = UPDATE_PARENT_ABOUT_PREDICTION; Send(msg->data2, (char *)&dMsg, sizeof(DriverMsg), (char *)NULL, 0); PrintDebug(me.ui, "merged. head is %d", me.trainNum); break; } case MERGE_TAIL: { if (me.tailMode) { PrintDebug(me.ui, "Double merge tail??"); break; } // Enters courier mode that passes dumb_train msg to 'real' controller me.tailMode = 1; me.headTid = msg->data2; clearReservation(me.trackManager, me.trainNum); Reply(msg->replyTid, (char*)1, 0); PrintDebug(me.ui, "Train %d is tail", me.trainNum); break; } case SEPARATE_TAIL: { if (!me.tailMode) { PrintDebug(me.ui, "Not in tail mode..??"); break; } me.tailMode = 0; me.headTid = 0; // TODO, behaviour is not clearly defined yet. // reserve my own track and prediction?? break; } case REPORT_INFO: { if (!me.tailMode) { PrintDebug(me.ui, "Report info Not in tail mode..??"); break; } // ASSUME ONLY 1 train when in tail mode. Send(me.trainId[0], (char*)msg, sizeof(DriverMsg), (char*)&me.info[0], sizeof(DumbDriverInfo)); // Reply the head that made query. Reply(me.headTid, (char*)&me.info[0], sizeof(DumbDriverInfo)); break; } case DELTA_DISTANCE: { if (!me.tailMode) { PrintDebug(me.ui, "Delta distance and not in tail mode..??"); break; } // ASSUME ONLY 1 train when in tail mode. Send(me.trainId[0], (char*)msg, sizeof(DriverMsg) - sizeof(Position), (char*)1, 0); // Reply the head that made query. Reply(me.headTid, (char*)1, 0); break; } case UPDATE_PARENT_ABOUT_PREDICTION: { if (!me.tailMode) { PrintDebug(me.ui, "%d Update Parent Not in tail mode..??", me.trainNum); break; } // ASSUME ONLY 1 train when in tail mode. Send(me.trainId[0], (char*)msg, sizeof(DriverMsg), (char*)NULL, 0); break; } case QUERY_STOP_COUNT: { // asuumption: no tree strucutre Reply(tid, (char *)&me.stoppedCount, sizeof(int)); break; } case MULTI_TRAIN_DRIVER_COURIER: { // nothing break; } case REVERSE_SPEED: { if (!me.tailMode) { PrintDebug(me.ui, "Reverse Speed Not in tail mode..??"); break; } for (int i = 0; i < me.numTrainInGroup; i++) { Send(me.trainId[i], (char *)msg, sizeof(DriverMsg), (char*)1, 0); } break; } case QUERY_STOPPING_DISTANCE: { if (!me.tailMode) { PrintDebug(me.ui, "Query stopping dist Not in tail mode..??"); break; } int stoppingDist = 0; // ASSUME ONLY 1 train when in tail mode. Send(me.trainId[0], (char*)msg, sizeof(DriverMsg), (char*)&stoppingDist, sizeof(int)); // Reply the head that made query. Reply(me.headTid, (char*)&stoppingDist, sizeof(int)); break; } case HIT_SECONDARY: { if (me.route.length != 0) { PrintDebug(me.ui, "Hit secondary rerouting.."); groupSetSpeed(&me, 0); } break; } case SET_FOLLOWING_DISTANCE: { me.minFollowingDist = msg->data2; me.maxFollowingDist = msg->data3; Reply(msg->replyTid, (char*)1, 0); break; } default: { PrintDebug(me.ui, "Not Handled %d", msg->type); } } // switch } // for }
static void trainSetSpeed(const int speed, const int stopTime, const int delayer, Driver* me) { char msg[4]; msg[1] = (char)me->trainNum; if (me->lastSensorActualTime > 0) { // a/d related stuff int newSpeed = speed >=0 ? speed : 0; int now = Time(me->timeserver) * 10; if (me->speed == newSpeed) { // do nothing } else if (me->speed == 0) { // accelerating from 0 int v0 = getVelocity(me); int v1 = me->v[newSpeed][ACCELERATE]; int t0 = now + 8; // compensate for time it takes to send to train int t1 = now + 8 + me->a[newSpeed]; poly_init(&me->adPoly, t0, t1, v0, v1); me->isAding = 1; me->lastReportDist = 0; me->adEndTime = t1; } else if (newSpeed == 0) { // decelerating to 0 int v0 = getVelocity(me); int v1 = me->v[newSpeed][DECELERATE]; int t0 = now + 8; // compensate for time it takes to send to train int t1 = now + 8 + getStoppingTime(me); poly_init(&me->adPoly, t0, t1, v0, v1); me->isAding = 1; me->lastReportDist = 0; me->adEndTime = t1; } } TrainDebug(me, "Train Setting Speed %d", speed); if (speed >= 0) { if (delayer) { TrainDebug(me, "Reversing speed.------- %d", speed); msg[0] = 0xf; msg[1] = (char)me->trainNum; msg[2] = (char)speed; msg[3] = (char)me->trainNum; Putstr(me->com1, msg, 4); //TrainDebug(me, "Next Sensor: %d %d", me->nextSensorIsTerminal, me->lastSensorIsTerminal); // Update prediction if (me->nextSensorIsTerminal) { me->nextSensorBox = me->nextSensorBox == EX ? EN : EX; //TrainDebug(me, "LAst Sensor: %d ", me->lastSensorVal); } else { int action = me->nextSensorVal%2 == 1 ? 1 : -1; me->nextSensorVal = me->nextSensorVal + action; } if (me->lastSensorIsTerminal) { me->lastSensorBox = me->lastSensorBox == EX ? EN : EX; } else { int action = me->lastSensorVal%2 == 1 ? 1 : -1; me->lastSensorVal = me->lastSensorVal + action; } float distTemp = me->distanceFromLastSensor; me->distanceFromLastSensor = me->distanceToNextSensor; me->distanceToNextSensor = distTemp; char valTemp = me->nextSensorVal; me->nextSensorVal = me->lastSensorVal; me->lastSensorVal = valTemp; char boxTemp = me->nextSensorBox; me->nextSensorBox = me->lastSensorBox; me->lastSensorBox = boxTemp; if (me->nextSensorIsTerminal || me->lastSensorIsTerminal){ char isTemp = me->nextSensorIsTerminal; me->nextSensorIsTerminal = me->lastSensorIsTerminal; me->lastSensorIsTerminal = isTemp; } // Reserve the track above train and future (covers case of init) // Update prediction updatePrediction(me); int reserveStatus = reserveMoreTrack(me, 0, me->d[speed][ACCELERATE][MAX_VAL]); // moving if (reserveStatus == RESERVE_FAIL) { reroute(me); } } else { //TrainDebug(me, "Set speed. %d %d", speed, me->trainNum); msg[0] = (char)speed; Putstr(me->com1, msg, 2); if (speed == 0) { int delayTime = stopTime + 500; Reply(me->stopDelayer, (char*)&delayTime, 4); } } if (speed > me->speed) { me->speedDir = ACCELERATE; } else if (speed < me->speed) { me->speedDir = DECELERATE; } me->speed = speed; } else { //TrainDebug(me, "Reverse... %d ", me->speed); DriverMsg delayMsg; delayMsg.type = SET_SPEED; delayMsg.timestamp = stopTime + 500; if (me->speedAfterReverse == -1) { delayMsg.data2 = (signed char)me->speed; } else { delayMsg.data2 = (signed char)me->speedAfterReverse; } //TrainDebug(me, "Using delayer: %d for %d", me->delayer, stopTime); Reply(me->delayer, (char*)&delayMsg, sizeof(DriverMsg)); msg[0] = 0; msg[1] = (char)me->trainNum; Putstr(me->com1, msg, 2); me->speed = 0; me->speedDir = DECELERATE; } }
void GUIVehicle::rerouteDRTStop(MSStoppingPlace* busStop) { SUMOTime intermediateDuration = TIME2STEPS(20); SUMOTime finalDuration = SUMOTime_MAX; if (myParameter->stops.size() >= 2) { // copy durations from the original stops intermediateDuration = myParameter->stops.front().duration; finalDuration = myParameter->stops.back().duration; } // if the stop is already in the list of stops, cancel all stops that come // after it and set the stop duration std::string line = ""; int destinations = 0; bool add = true; for (auto it = myStops.begin(); it != myStops.end(); it++) { if (!it->reached && destinations < 2 && it->busstop != nullptr) { line += it->busstop->getID(); destinations++; } if (it->busstop == busStop) { it->duration = finalDuration; myStops.erase(++it, myStops.end()); add = false; break; } else { it->duration = MIN2(it->duration, intermediateDuration); } } if (destinations < 2) { line += busStop->getID(); } if (add) { // create new stop SUMOVehicleParameter::Stop stopPar; stopPar.busstop = busStop->getID(); stopPar.lane = busStop->getLane().getID(); stopPar.startPos = busStop->getBeginLanePosition(); stopPar.endPos = busStop->getEndLanePosition(); stopPar.duration = finalDuration; stopPar.until = -1; stopPar.triggered = false; stopPar.containerTriggered = false; stopPar.parking = false; stopPar.index = STOP_INDEX_FIT; stopPar.parametersSet = STOP_START_SET | STOP_END_SET; // clean up prior route to improve visualisation, ensure that the stop can be added immediately ConstMSEdgeVector edges = myRoute->getEdges(); edges.erase(edges.begin(), edges.begin() + getRoutePosition()); edges.push_back(&busStop->getLane().getEdge()); replaceRouteEdges(edges, -1, 0, "DRT.tmp", false, false, false); std::string errorMsg; // add stop addStop(stopPar, errorMsg); } const bool hasReroutingDevice = getDevice(typeid(MSDevice_Routing)) != nullptr; SUMOAbstractRouter<MSEdge, SUMOVehicle>& router = hasReroutingDevice ? MSRoutingEngine::getRouterTT() : MSNet::getInstance()->getRouterTT(); // reroute to ensure the new stop is reached reroute(MSNet::getInstance()->getCurrentTimeStep(), "DRT", router); myParameter->line = line; assert(haveValidStopEdges()); }
void driver() { Driver me; initDriver(&me, 1); unsigned int naggCount = 0; unsigned int updateStoppingDistanceCount = 0; for (;;) { int tid = -1; DriverMsg msg; msg.data2 = -1; msg.data3 = -1; msg.replyTid = -1; Receive(&tid, (char*)&msg, sizeof(DriverMsg)); if (tid != me.delayer && tid != me.stopDelayer) { Reply(tid, (char*)1, 0); } const int replyTid = msg.replyTid; switch (msg.type) { case SET_SPEED: { //TrainDebug(&me, "Set speed from msg"); trainSetSpeed(msg.data2, getStoppingTime(&me), (msg.data3 == DELAYER), &me); if (msg.data3 != DELAYER) { //TrainDebug(&me, "Replied to %d", replyTid); Reply(replyTid, (char*)1, 0); sendUiReport(&me); break; } else if (me.route.length != 0) { // Delayer came back. Reverse command completed me.stopCommited = 0; // We're moving again. // We've completed everything up to the reverse node. me.routeRemaining = me.stopNode+1; me.previousStopNode = me.routeRemaining; me.distanceFromLastSensorAtPreviousStopNode = me.distanceFromLastSensor; // Calculate the next stop node. updateStopNode(&me); me.nextSetSwitchNode = -1; updateSetSwitch(&me); // if the reverse is last node, nothing to do // if it isn't.. it should speed up again. } } case DELAYER: { //TrainDebug(&me, "delayer come back."); break; } case STOP_DELAYER: { // To prevent the first receive from this delayer if (me.lastSensorActualTime > 0 && me.speed == 0 && !me.isAding) { TrainDebug(&me, "releasing reserveration"); int reserveStatus = reserveMoreTrack(&me, 1, 0); if (reserveStatus == RESERVE_FAIL) { TrainDebug(&me, "WARNING: unable to reserve during init"); } } break; } case SENSOR_TRIGGER: { // only handle sensor reports in primary + secondary prediction if not position finding int sensorReportValid = 0; TrackLandmark conditionLandmark; int condition; int isSensorReserved = QueryIsSensorReserved(&me, msg.data2, msg.data3); if (me.positionFinding) { sensorReportValid = 1; me.lastSensorUnexpected = 1; //FinishPositionFinding(me.trainNum, me.trainController); } else if (isSensorReserved) { //TrainDebug(&me, "Predictions."); for (int i = 0; i < me.numPredictions; i ++) { TrackLandmark predictedSensor = me.predictions[i].sensor; //printLandmark(&me, &predictedSensor); if (predictedSensor.type == LANDMARK_SENSOR && predictedSensor.num1 == msg.data2 && predictedSensor.num2 == msg.data3) { sensorReportValid = 1; if (i != 0) { TrainDebug(&me, "Trigger Secondary"); // secondary prediction, need to do something about them conditionLandmark = me.predictions[i].conditionLandmark; condition = me.predictions[i].condition; me.lastSensorUnexpected = 1; if (conditionLandmark.type == LANDMARK_SWITCH) { TrackMsg setSwitch; setSwitch.type = UPDATE_SWITCH_STATE; TrainDebug(&me, "UPDATE SWITCH STATE"); setSwitch.landmark1 = conditionLandmark; setSwitch.data = condition; Send(me.trackManager, (char*)&setSwitch, sizeof(TrackMsg), (char *)1, 0); } // Stop and then try to reroute. reroute(&me); } else { me.lastSensorUnexpected = 0; } } } } if (sensorReportValid) { updateRoute(&me, msg.data2, msg.data3); me.lastSensorBox = msg.data2; // Box me.lastSensorVal = msg.data3; // Val me.lastSensorIsTerminal = 0; me.lastSensorActualTime = msg.timestamp; dynamicCalibration(&me); me.lastSensorPredictedTime = me.nextSensorPredictedTime; TrackNextSensorMsg trackMsg; QueryNextSensor(&me, &trackMsg); // Reserve the track above train and future (covers case of init) for (int i = 0; i < trackMsg.numPred; i++) { me.predictions[i] = trackMsg.predictions[i]; } me.numPredictions = trackMsg.numPred; int reserveStatus = reserveMoreTrack(&me, me.positionFinding, getStoppingDistance(&me)); if (reserveStatus == RESERVE_FAIL) { if (!me.positionFinding) { reroute(&me); } else { TrainDebug(&me, "WARNING: unable to reserve during init"); } } TrackSensorPrediction primaryPrediction = me.predictions[0]; me.calibrationStart = msg.timestamp; me.calibrationDistance = primaryPrediction.dist; int dPos = 50 * getVelocity(&me) / 100000.0; me.lastSensorDistanceError = -(int)me.distanceToNextSensor - dPos; me.distanceFromLastSensor = dPos; me.distanceToNextSensor = primaryPrediction.dist - dPos; me.lastPosUpdateTime = msg.timestamp; if (primaryPrediction.sensor.type != LANDMARK_SENSOR && primaryPrediction.sensor.type != LANDMARK_END) { TrainDebug(&me, "QUERY_NEXT_SENSOR_FROM_SENSOR ..bad"); } me.nextSensorIsTerminal = (primaryPrediction.sensor.type == LANDMARK_END); me.nextSensorBox = primaryPrediction.sensor.num1; me.nextSensorVal = primaryPrediction.sensor.num2; me.nextSensorPredictedTime = msg.timestamp + me.distanceToNextSensor*100000 / getVelocity(&me); updatePosition(&me, msg.timestamp); sendUiReport(&me); if (me.positionFinding) { trainSetSpeed(0, getStoppingTime(&me), 0, &me); // Found position, stop. me.positionFinding = 0; me.currentlyLost = 0; } } break; } case NAVIGATE_NAGGER: { updatePosition(&me, msg.timestamp); if (me.routeRemaining != -1) { if (!me.stopCommited) { if (shouldStopNow(&me)) { if (me.route.nodes[me.stopNode].num == REVERSE) { //TrainDebug(&me, "Navi reversing."); const int speed = -1; trainSetSpeed(speed, getStoppingTime(&me), 0, &me); } else { //TrainDebug(&me, "Navi Nagger stopping."); const int speed = 0; // Set speed zero. trainSetSpeed(speed, getStoppingTime(&me), 0, &me); me.route.length = 0; // Finished the route. me.testMode = 0; } me.stopCommited = 1; me.useLastSensorNow = 0; me.stopNow = 0; me.stopSensorHit = 0; } else { if ((++updateStoppingDistanceCount & 15) == 0) updateStopNode(&me); } } } if (me.nextSetSwitchNode != -1 && (++me.setSwitchNaggerCount & 3) == 0) { trySetSwitch_and_getNextSwitch(&me); } if (me.rerouteCountdown-- == 0) { if (me.testMode) { int reserveStatus = reserveMoreTrack(&me, 0, me.d[8][ACCELERATE][MAX_VAL]); // moving if (reserveStatus == RESERVE_FAIL) { reroute(&me); } else { me.nextSetSwitchNode = -1; updateSetSwitch(&me); trainSetSpeed(8, 0, 0, &me); } } else { // reroute if (me.route.length != 0) { setRoute(&me, &(me.routeMsg)); } } } if ((++naggCount & 15) == 0) sendUiReport(&me); break; } case SET_ROUTE: { Reply(replyTid, (char*)1, 0); me.routeMsg = msg; setRoute(&me, &msg); break; } case BROADCAST_UPDATE_PREDICTION: { updatePrediction(&me); int reserveStatus = reserveMoreTrack(&me, 0, getStoppingDistance(&me)); // moving if (reserveStatus == RESERVE_FAIL) { reroute(&me); } break; } case BROADCAST_TEST_MODE: { me.testMode = 1; setRoute(&me, &msg); break; } case FIND_POSITION: { me.positionFinding = 1; trainSetSpeed(5, 0, 0, &me); break; } default: { TrainDebug(&me, "Not suppported train message type."); } } } }