/** * @RDE_HEADER@ * ObjectMap for EffectCluster * $Id$ * $HeadURL$ * $Author$ */ #include "object_effect_cluster.h" #include "Utils/polygons.h" #include "handler_charge_administration.h" #include "handler_charge_effect.h" #include "handler_effector.h" #include "object_charge_effect.h" #include "object_effector.h" #include "settings.h" #include "special_area_provider.h" #include #include #include #include #include #include #include #include #include #include #include #include #include // ToDo: mark the end of aes enum and update following (check Endemarke) static_assert((int)AreaEffectSimulation::AesType_def::AtEffectClusterGunToTargetLine < 100, "id may overlap at runtime"); using DetonationLocationRequest = CentralEffectSimulation::DetonationLocationRequest; using DetonationLocationReply = CentralEffectSimulation::DetonationLocationReply; // // AreaEffectSimulation // namespace AreaEffectSimulation { inline std::vector toEffectorVec(const QList effectorDataVec) { if (effectorDataVec.size() == 0) return {}; std::vector ret; for (const auto &effectorData : effectorDataVec) { AreaEffectSimulation::EffectorData data; data.angleOfFall() = effectorData.angleOfFall; data.chargeAmount() = effectorData.chargeAmount; data.chargeStruct().chargeTypeId() = effectorData.chargeTypeId; data.flightTime() = effectorData.flightTime; data.chargeStruct().igniterTypeId() = effectorData.igniterTypeId; data.chargeStruct().igniterFunctionId() = effectorData.igniterFunctionId; data.interval() = effectorData.interval; data.mhpPosition().lat() = effectorData.mhpPosition.y(); data.mhpPosition().lon() = effectorData.mhpPosition.x(); data.mhpPosition().z() = effectorData.mhpPosition.z(); data.mhpRotation() = effectorData.mhpRotation; data.mhpScatterLenght() = effectorData.mhpScatterLenght; data.mhpScatterLengthDetonationHeight() = effectorData.mhpScatterLengthDetonationHeight; data.mhpScatterWidth() = effectorData.mhpScatterWidth; data.mhpScatterWidthDetonationHeight() = effectorData.mhpScatterWidthDetonationHeight; data.participantId() = effectorData.participantId; data.peakHeight() = effectorData.peakHeight; data.trajectoryRelative() = TrajectoryRelative::fromString(effectorData.trajectory); data.properties() = effectorData.properties.toStdString(); ret.push_back(data); } return ret; } ObjectEffectCluster::ObjectEffectCluster(MainHandler *mainHandler, QObject *parent) : ObjectBaseTmpl(mainHandler, QStringLiteral("EffectCluster"), parent) , m_singleExplosionTriggerTimer(new QTimer(this)) , m_preStartTimer(new QTimer(this)) , m_automatonTimer(new QTimer(this)) , m_lastSentTriggeredExplosionNumber(-1) , m_chargeCountUntilLastStart(0) , m_explosionIndex(0) { // connect(m_singleExplosionTriggerTimer, &QTimer::timeout, // this, &ObjectEffectCluster::checkNextSingleExplosion); connect(m_preStartTimer, &QTimer::timeout, this, &ObjectEffectCluster::checkNextFire); connect(m_automatonTimer, &QTimer::timeout, this, &ObjectEffectCluster::checkAutomaton); m_automatonTimer->setInterval(500); m_bSupportsPausing = true; } ObjectEffectCluster::~ObjectEffectCluster() { removeTopics(); } bool ObjectEffectCluster::isOnlyObjectNameModifiable() const { const bool bBaseIsOnlyNameModifiable = ObjectBase::isOnlyObjectNameModifiable(); const qint64 nowMsec = partitionTimeMSec(); const qint64 activationMsec = activationTime().toMSecsSinceEpoch(); qint64 preStartMsec = activationMsec - m_temporaryData.estimatedMaxFlightDurationMSec; // estimatedMaxFligh contains a buffer time (10% + 2sec) const bool bReached = nowMsec >= preStartMsec; DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << ", bBaseIsOnlyNameModifiable=" << bBaseIsOnlyNameModifiable << ", m_estimatedMaxFlightDurationMSec=" << m_temporaryData.estimatedMaxFlightDurationMSec << ", nowMsec=" << nowMsec << ", activationMsec=" << activationMsec << ", prePreStartMsec=" << preStartMsec << ", leftMsec=" << (preStartMsec - nowMsec) << ", bReached=" << bReached << ", m_temporaryData.isObjectModifiable=" << m_temporaryData.isObjectModifiable; return bBaseIsOnlyNameModifiable || !m_temporaryData.isObjectModifiable; } bool ObjectEffectCluster::isObjectModifiable() const { return ObjectBase::isObjectModifiable(); // ToDo: check: && m_temporaryData.isObjectModifiable } Core::FunctionReplyObject ObjectEffectCluster::createObject(const Database::AesEffectCluster &data, const QVector &effectorDatas) { Core::FunctionReplyObject ret; // some plausibility checks ret = checkObjectData(data); if (ret.error()) return ret; if (data.impactCount > AreaEffectSimulation::s_maxTrajectoriesPerExplosionCluster) return Core::FunctionReplyObject(data.id, tr_lts("dds", "Error with explosion cluster with id %1, impactCount exceeds limit.")).arg(m_data.id); const bool bFromOpwUser = data.id == 0; // No need to create object, if there is not enough charge left. // HLA Interface is using externalId... // external (externalId>0) -> no claim required // external (externalId==0) -> claim required // NASAT: AirSupport -> no claim required const bool bDoClaimPlannedCharge = (data.id == 0) && (data.externalId <= 0) && data.effectSourceId != ProjectDataManagement::EffectSource::ES_ADLER && data.effectSourceId != ProjectDataManagement::EffectSource::ES_ADLER_STANDALONE && (m_mainHandler->gearClassOfChargeTypeId(data.chargeTypeId) != ProjectDataManagement::GearClass_def::AirSupport); if (bDoClaimPlannedCharge) // new, not read from db ret = m_mainHandler->chargeAdministrationHandler()->claimPlannedCharge(data.chargeTypeId, data.partyId, data.impactCount); if (ret.error()) return ret; // EffectClusters are always central ret = ObjectBaseTmpl::createObject(data, false, true); if (ret.error()) { if (bDoClaimPlannedCharge) m_mainHandler->chargeAdministrationHandler()->claimPlannedCharge(data.chargeTypeId, data.partyId, (-1) * data.impactCount); return ret; } // If an EffectCluster is EsPrepared (EffectStart set), maybe the calculations // to generate trajectories etc are already done. // And the corresponding topics are most likely already published. // SvcAes could be restarted _after_ calculations and before effectStart (maybe also during effectDuration) // The initialCaculation fails due to not enough time for the ammo to fly. // In that case, the EffectCluster is marked invalid. // But if the user re-enables the EffectCluster impacts or trajectories // from former calculations are _accidentally_ considered and displayed. // To avoid that situation: removeTopics(); // keep in mind: if no participantId is provided: no gunToTargetLine will be calculated if (isMhpRotationProvided()) m_temporaryData.calculatedMhpRotationNorthDeg = m_data.mhpRotationRequest; bool isRocket = m_mainHandler->dds()->pdmDtc()->isRocket(data.chargeTypeId); // -- Module MHP // If there are not MHP Positions then we generate them. // normaly this will also start the trajectory module // uses polygones to create mhp inside the given geometry DEBUG_LOG(m_logger) << " start check MHP Mpdule"; auto effectorDataCopy = effectorDatas; if (data.effectSourceId == ProjectDataManagement::EffectSource::ES_ADLER_STANDALONE && !isRocket) { DEBUG_LOG(m_logger) << " start Generate"; effectorDataCopy = generateMHPs(effectorDatas, data.explosionGeometryId); DEBUG_LOG(m_logger) << " end MHP Mpdule"; } // -- end Module MHP // setupFiringOrigin if (bFromOpwUser && isOriginatorParticipantIdProvided()) { Core::FunctionReplyObject retSFO = setupFiringOrigin(); m_temporaryData.annotationString = retSFO.translated().toStdString(); if (!isEffectClusterMarkedValid()) m_data.effectStart = QDateTime(); } Core::FunctionReplyObject retCPC = checkPreConditionsComplete(); m_temporaryData.annotationString = retCPC.translated().toStdString(); if (!isEffectClusterMarkedValid()) m_data.effectStart = QDateTime(); if (data.id <= 0) // new, not read from db { m_data.originalEffectDuration = data.effectDuration; m_data.originalEffectStart = data.effectStart; } DEBUG_LOG(m_logger) << " start check trajectory Module"; // -- start Module Trajectory // If there are no Trajectories then we generate them. // normaly this will normaly happen when there were no MHPs too. // but could possibly used for other stuff // uses function calls to TAB -> to generate Trajectores. // might fail and end with no usable EffectCluster if (data.effectSourceId == ProjectDataManagement::EffectSource::ES_ADLER_STANDALONE && data.effectStateId != AesEffectState::EsExpired && data.effectStateId != AesEffectState::EsRemoved && !isRocket) { // if (m_mainHandler->dds()->ppmDtc()->isSaaConnected(m_data.partyId) && m_trajectoryCont.size() <= 0) { DEBUG_LOG(m_logger) << " start check trajectory Mpdule"; // NOT NOW sendCalculateTrajectoryAdlerRequest(effectorDataCopy); } } // -- END Module Trajectory QVector finalEffectorDatas; for (auto effectorData : effectorDataCopy) { if (effectorData.entityId == 0 && effectorData.id == 0) // need to create { effectorData.entityId = m_data.id; if (!m_mainHandler->db()->insert(effectorData)) // ID will be set! ERROR_LOG(m_logger) << "Error while inserting effector Data " << m_mainHandler->db()->getLastError().text(); } finalEffectorDatas.append(effectorData); } for (auto effectorData : finalEffectorDatas) DEBUG_LOG(m_logger) << "Created Object new effector " << effectorData.id << " for " << effectorData.entityId; // if (data.effectStateId > AreaEffectSimulation::AesEffectState::EsExpired) setEffectors(finalEffectorDatas); if (!data.properties.isEmpty()) { QJsonObject propertiesObject = QJsonDocument::fromJson(QByteArray::fromStdString(data.properties.toStdString())).object(); m_data.isUsingEffectStateReceived = propertiesObject.value(QStringLiteral("useReceived")).toBool(); } // cargeCounting m_data.impactCount = 0; for (const auto &effector : m_effectorData) { m_data.impactCount += effector.chargeAmount; } if (m_data.effectStateId != AesEffectState::EsCreated && m_data.effectStateId != AesEffectState::EsPrepared && m_data.effectStateId != AesEffectState::EsReceived) m_firstTimeReady = false; // set reaminExplosion = impactCount m_data.remainExplosionNumber = m_data.impactCount; // -- Module FireAutomaton // If we are an internal Effect Cluster we need someone who will tell the Effectors // for action // this will be initatlizes here // check if we have a firingType QJsonObject probObject = QJsonDocument::fromJson(QByteArray::fromStdString(data.properties.toStdString())).object(); FiringType firingType = FiringType::ReportReadyToFire; if (probObject.contains(AdlerJson::JsonKeys::ABFEUERUNGSART())) firingType = static_cast(probObject[AdlerJson::JsonKeys::ABFEUERUNGSART()].toInt()); if (data.effectSourceId == ProjectDataManagement::EffectSource::ES_ADLER_STANDALONE && data.effectStateId != AesEffectState::EsExpired && data.effectStateId != AesEffectState::EsRemoved && firingType != FiringType::FireOnCommand) { bool automaInitOk = true; if (m_fireAutomaton) m_fireAutomaton->deleteLater(); m_fireAutomaton = new FireAutomaton(this); const auto &gearTypeIds = m_mainHandler->dds()->pdmDtc()->gearTypeIdFromChargeTypeId(m_data.chargeTypeId); int cadence = 35000; if (gearTypeIds.size()) cadence = m_mainHandler->dds()->pdmDtc()->cadence(gearTypeIds.at(0)); DEBUG_LOG(m_logger) << "Fire Automaton cadence: " << cadence; switch (firingType.underlying()) { case FiringType::FireNow: case FiringType::FireWithIndicationTOT: case FiringType::FireWithIndicationTTF: case FiringType::ReportReadyToFire: { if (m_data.effectDuration != DEFAULT_EFFECT_DURATION) { // use duration const int maxRoundEffector = std::ceil((double)m_data.impactCount / m_effectorData.size()); // when using the effect duration, we need enough time to spread the volleys inside the duration. // using multiple effectors, we can split the charges within the duration, but never faster than the cadence allowes // so we will check if duration is at least cadence times impactCount if (m_data.effectDuration <= ((cadence * maxRoundEffector) + 500)) // add some more time return Core::FunctionReplyObject(data.id, tr_lts("dds", "Error while creating EffectCluster, duration not valid")); DEBUG_LOG(m_logger) << "Update Init after new trajectory Fire Automaton with effect duration: " << m_data.effectDuration; automaInitOk = m_fireAutomaton->init(m_data.effectDuration, FireAutomaton::TimeType::Duration, m_data.impactCount, m_effectorData.keys().toVector(), QStringLiteral("next*")); } else { // use cadence DEBUG_LOG(m_logger) << "Init Fire Automaton with cadence: " << cadence; automaInitOk = m_fireAutomaton->init(cadence, FireAutomaton::TimeType::Cadence, m_data.impactCount, m_effectorData.keys().toVector(), QStringLiteral("all*")); } } default: break; } if (!automaInitOk) return Core::FunctionReplyObject(data.id, tr_lts("dds", "Error while creating EffectCluster, fire automaton not valid")); // connects // m_fireAutomaton->stop(); connect(m_fireAutomaton, &FireAutomaton::fire, this, QOverload::of(&ObjectEffectCluster::sendFireCommand)); connect(m_fireAutomaton, &FireAutomaton::finished, this, &ObjectEffectCluster::onAutomatonFinished); if (!data.effectStart.isNull()) m_automatonTimer->start(); } // -- END Module FireAutomaton DEBUG_LOG(m_logger) << "end of create Object"; return ret; } Core::FunctionReplyObject ObjectEffectCluster::updateObject(const Database::AesEffectCluster &data, const QVector &effectorDatas) { DEBUG_LOG(m_logger) << "updateObject properties=" << data.properties; Core::FunctionReplyObject ret; // some plausibility checks ret = checkObjectData(data); if (ret.error()) return ret; if (data.impactCount > AreaEffectSimulation::s_maxTrajectoriesPerExplosionCluster) return Core::FunctionReplyObject(data.id, tr_lts("dds", "Error with explosion cluster with id %1, impactCount exceeds limit.")).arg(m_data.id); ret = ObjectBaseTmpl::updateObject(data); if (ret.error() || (isOnlyObjectNameModifiable() && (data.id > 0))) // data.id > 0 -> negative values from oc always get a full update return ret; bool updateEffectCount = false; // if (data.id == 0) // should not happen, maybe an assert here ? // return ret; const QJsonObject probObject = QJsonDocument::fromJson(QByteArray::fromStdString(data.properties.toStdString())).object(); FiringType firingType = FiringType::FireNow; if (probObject.contains(AdlerJson::JsonKeys::ABFEUERUNGSART())) firingType = static_cast(probObject[AdlerJson::JsonKeys::ABFEUERUNGSART()].toInt()); // only allow some fields Database::AesEffectCluster tmpData(m_data); tmpData.id = std::abs(data.id); tmpData.name = data.name; tmpData.element = data.element; if (firingType == FiringType::FireWithIndicationTTF && probObject.contains(AdlerJson::JsonKeys::ABFEUERUNGSZEIT()) && effectorDatas.at(0).flightTime > 0) { const double ttf_time = probObject[AdlerJson::JsonKeys::ABFEUERUNGSZEIT()].toDouble(); TRACE_LOG(m_logger) << "update ttf and effect start: ttf_time" << ttf_time << " efector flight time: " << effectorDatas.at(0).flightTime; TRACE_LOG(m_logger) << "new ttf " << QDateTime::fromMSecsSinceEpoch((ttf_time * 1000)).toString(); TRACE_LOG(m_logger) << "effect start " << QDateTime::fromMSecsSinceEpoch((ttf_time * 1000) + effectorDatas.at(0).flightTime).toString(); tmpData.effectStart = QDateTime::fromMSecsSinceEpoch((ttf_time * 1000) + effectorDatas.at(0).flightTime); } else tmpData.effectStart = data.effectStart; tmpData.effectDuration = data.effectDuration; tmpData.tla = data.tla; tmpData.explosionGeometryId = data.explosionGeometryId; tmpData.impactPosition = data.impactPosition; tmpData.rotationAngle = data.rotationAngle; tmpData.impactWidth = data.impactWidth; tmpData.impactLength = data.impactLength; tmpData.impactCount = data.impactCount; tmpData.mhpCountRequest = data.mhpCountRequest; // tmpData.fuseTypeId = data.fuseTypeId; tmpData.peakHeight = data.peakHeight; tmpData.flightDuration = data.flightDuration; tmpData.partyId = data.partyId; tmpData.participantId = data.participantId; tmpData.firingCenterPosition = data.firingCenterPosition; tmpData.chargeTypeId = data.chargeTypeId; tmpData.fireRequestTime = data.fireRequestTime; tmpData.altitude = data.altitude; tmpData.firePurpose = data.firePurpose; // [GUEZE-3145] Automatische Veraenderung bei Steilfeuer Dialog -> Stoeren tmpData.firePurposeId = data.firePurposeId; tmpData.effectMarkingInTa = data.effectMarkingInTa; tmpData.effectStateId = data.effectStateId; // overwrite for oc tmpData.properties = data.properties; if (!isOnlyObjectNameModifiable()) // check again, this time also if oc { tmpData.originalEffectDuration = data.effectDuration; tmpData.originalEffectStart = data.effectStart; tmpData.impactCount = data.impactCount; tmpData.mhpCountRequest = data.mhpCountRequest; tmpData.mhpRotationRequest = data.mhpRotationRequest; } tmpData.esmParticipantIds = LtsUtils::IntArray(data.esmParticipantIds()); tmpData.launchVelocity = data.launchVelocity; tmpData.launchAngleDeg = data.launchAngleDeg; tmpData.dropAngle = data.dropAngle; tmpData.tapTimepoint = data.tapTimepoint; tmpData.lcPrf = data.lcPrf; tmpData.lcTermination = data.lcTermination; tmpData.cloudBase = data.cloudBase; // // update effect state // if (m_data.esmParticipantIds() != data.esmParticipantIds()) // data.esmParticipantIds().empty() ? tmpData.effectorState = AesEffectorState::EsReadyToFire : tmpData.effectorState = AesEffectorState::EsNotReadyToFire; m_data.esmParticipantIds() = data.esmParticipantIds(); // chargeType update if (effectorDatas.size() > 0) { tmpData.chargeTypeId = effectorDatas.at(0).chargeTypeId; } if (!m_mainHandler->db()->update(tmpData)) return Core::FunctionReplyObject(data.id, tr_lts("dds", "Error updating effect cluster with id %1 in database.")).arg(m_data.id); QSet oldIds; QSet newIds; bool updateMHP = false; DEBUG_LOG(m_logger) << " impactpos " << (m_data.impactPosition != data.impactPosition) << " impactLen " << (m_data.impactLength != data.impactLength) << " width " << (m_data.impactWidth != data.impactWidth); if (data.effectSourceId == ProjectDataManagement::EffectSource::ES_ADLER_STANDALONE && (m_data.impactPosition != data.impactPosition || m_data.impactLength != data.impactLength || m_data.impactWidth != data.impactWidth || m_data.tla != data.tla)) { updateMHP = true; } int oldImpactCount = 0; int newImpactCount = 0; for (const auto &oldEffector : qAsConst(m_effectorData)) oldImpactCount += oldEffector.chargeAmount; for (const auto &newEffector : qAsConst(effectorDatas)) newImpactCount += newEffector.chargeAmount; if (oldImpactCount != newImpactCount) updateEffectCount = true; // update internal data m_data = std::move(tmpData); auto newEffectorDatas = effectorDatas; bool updateTrajectory = false; if (updateMHP) { updateTrajectory = true; // generate new MTP if changed DEBUG_LOG(m_logger) << "Update need to update efecotrs"; newEffectorDatas = generateMHPs(effectorDatas, data.explosionGeometryId); } QVector newEffectorData; for (auto effectorData : newEffectorDatas) { for (const auto &oldEffector : m_effectorData) { // DEBUG_LOG(m_logger) << " looking through old effectorData: " << oldEffector.id << " with " << oldEffector.entityId << " and participantId " << // oldEffector.participantId //<<" same as " << effectorData.entityId << " " << effectorData.participantId; oldIds.insert(oldEffector.id); if (oldEffector.entityId == effectorData.entityId && oldEffector.participantId == effectorData.participantId) { effectorData.id = oldEffector.id; break; } } if (effectorData.id > 0) { DEBUG_LOG(m_logger) << "UPDATE EFFECTORDATA "; if (!m_mainHandler->db()->update(effectorData)) return Core::FunctionReplyObject(data.id, tr_lts("dds", "Error updating effectorData with id %1 in database.")).arg(effectorData.id); newIds.insert(effectorData.id); } else { DEBUG_LOG(m_logger) << "UPDATE EFFECTORDATA "; if (!m_mainHandler->db()->insert(effectorData)) return Core::FunctionReplyObject(data.id, tr_lts("dds", "Error updating effectorData with id %1 in database.")).arg(effectorData.id); newIds.insert(effectorData.id); } newEffectorData.append(effectorData); } //(delete no longer needed effectorData) QSet idsToDelete = oldIds - newIds; if (idsToDelete.size()) { for (auto idToDelete : idsToDelete) m_mainHandler->db()->removeAesEffectorData(idToDelete); } // set new Effectors to internal Map and then publish setEffectors(newEffectorData); if (data.effectSourceId != ProjectDataManagement::EffectSource::ES_ADLER && data.effectSourceId != ProjectDataManagement::EffectSource::ES_ADLER_STANDALONE && (data.externalId <= 0) && m_mainHandler->gearClassOfChargeTypeId(m_data.chargeTypeId) != ProjectDataManagement::GearClass_def::AirSupport) // internal { // this is for party change or charge type change if (data.partyId != m_data.partyId || data.chargeTypeId != m_data.chargeTypeId) { ret = m_mainHandler->chargeAdministrationHandler()->claimPlannedCharge(m_data.chargeTypeId, m_data.partyId, (-1) * m_data.impactCount); if (ret.error()) return ret; ret = m_mainHandler->chargeAdministrationHandler()->claimPlannedCharge(data.chargeTypeId, data.partyId, data.impactCount); if (ret.error()) return ret; } else // this is the case when cannonCount or cannonRounds changed { int diff = data.impactCount - m_data.impactCount; if (diff != 0) { DEBUG_LOG(m_logger) << "Claiming planned charge " << diff; ret = m_mainHandler->chargeAdministrationHandler()->claimPlannedCharge(data.chargeTypeId, data.partyId, diff); if (ret.error()) return ret; } } } // reset some data: m_aiv2.form().clear(); // enforce recalculation m_effects.singleEffects().clear(); m_aivGunToTargetTrajectoryMap.clear(); // enforce recalculation m_temporaryData.reset(); // estimatedMaxFlightDurationMSec, mhPoints, mhWidth... // keep in mind: if no participantId is provided: no gunToTargetLine will be calculated if (isMhpRotationProvided()) m_temporaryData.calculatedMhpRotationNorthDeg = m_data.mhpRotationRequest; m_data.firingCenterPosition = LtsUtils::QPoint3D(); // Core::GeoPosition(); // setupFiringOrigin if (isOriginatorParticipantIdProvided()) { Core::FunctionReplyObject retSFO = setupFiringOrigin(); m_temporaryData.annotationString = retSFO.translated().toStdString(); if (!isEffectClusterMarkedValid()) m_data.effectStart = QDateTime(); } // do checkPreConditions { Core::FunctionReplyObject retCPC = checkPreConditionsComplete(); if (retCPC.error()) abortPreStartTimer(); m_temporaryData.annotationString = retCPC.translated().toStdString(); if (!isEffectClusterMarkedValid()) m_data.effectStart = QDateTime(); } if (!m_data.effectStart.isValid()) abortPreStartTimer(); // update reaminExplosion = impactCount if changed if (updateEffectCount) m_data.remainExplosionNumber = newImpactCount; else m_data.remainExplosionNumber = data.remainExplosionNumber; m_publishDataTimer->start(); checkEnterNewState(); if (m_fireAutomaton) { DEBUG_LOG(m_logger) << "!!!!!!!!!!!!! AUOTMATON NEDD UPDATE need update keys: " << m_effectorData.keys() << " duration " << data.effectDuration << "used duration " << data.effectDuration - (int)m_effectorData.begin().value().flightTime << " impactCount_ " << data.impactCount; if (m_data.effectDuration != DEFAULT_EFFECT_DURATION) { DEBUG_LOG(m_logger) << "Update Init Fire Automaton with effect duration: " << m_data.effectDuration; // use duration m_fireAutomaton->init(m_data.effectDuration, FireAutomaton::TimeType::Duration, m_data.impactCount, m_effectorData.keys().toVector(), QStringLiteral("next*")); } else { // use cadence const auto &gearTypeIds = m_mainHandler->dds()->pdmDtc()->gearTypeIdFromChargeTypeId(m_data.chargeTypeId); int cadence = 35000; if (gearTypeIds.size()) cadence = m_mainHandler->dds()->pdmDtc()->cadence(gearTypeIds.at(0)); DEBUG_LOG(m_logger) << "Update Init Fire Automaton with cadence: " << cadence; m_fireAutomaton->init(cadence, FireAutomaton::TimeType::Cadence, m_data.impactCount, m_effectorData.keys().toVector(), QStringLiteral("all*")); } if (m_data.effectStateId == AesEffectState::EsPrepared) m_automatonTimer->start(); else m_automatonTimer->stop(); } if (updateTrajectory) { DEBUG_LOG(m_logger) << "need update trajectory"; sendCalculateTrajectoryAdlerRequest(newEffectorData); } publishAiv(); return Core::FunctionReplyObject(data.id); // data not m_data for the possible negative id in the oc-laptop } bool ObjectEffectCluster::initialCalculations(QString *reasonOut) { TRACE_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " START" << " id/name= '" << m_data.name << "'/" << m_data.id; // // calculate the subAreas / mtps: // DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " START" << " id/name= '" << m_data.name << "'/" << m_data.id << " IS_VULCANO = " << isVulcano(); if (!isVulcano()) { double singleWidthOut, singleHeightOut; singleWidthOut = singleHeightOut = 0; DEBUG_LOG(m_logger) << "Calculate new zone with TLA " << m_data.tla; const LtsCoordTrans::UtmCoordinate &impactPositionUtm = LtsCoordTrans::UtmCoordinate::fromGeographic(m_data.impactPosition); // impactPosition = cornerBottomLeft const Polygons::AreaBottomLeft areaBLOrgWithoutTla(impactPositionUtm, m_data.impactWidth, m_data.impactLength, m_data.rotationAngle); const Polygons::AreaBottomLeft areaBlWithTla = Polygons::explosionClusterAddTla(areaBLOrgWithoutTla, m_data.tla); DEBUG_LOG(m_logger) << "Area without TLA Width" << areaBLOrgWithoutTla.m_width; DEBUG_LOG(m_logger) << "Area with TLA Width" << areaBlWithTla.m_width; const QPointF center = Polygons::bottomLeft2CenterPoint(m_data.impactPosition, m_data.impactWidth, m_data.impactLength, 0.0); const LtsCoordTrans::UtmCoordinate ¢erUtm = LtsCoordTrans::UtmCoordinate::fromGeographic(center, impactPositionUtm.zone); const LtsCoordTrans::UtmCoordinate &firingOriginUtm = LtsCoordTrans::UtmCoordinate::fromGeographic(m_data.firingCenterPosition.toPointF()); QVector mtps_asUtm; if (m_data.explosionGeometryId.underlying() == AreaEffectSimulation::AesExplosionGeometry_def::EgRadial) mtps_asUtm = explosionClusterRadialToMtps(areaBlWithTla, m_data.mhpCountRequest, firingOriginUtm, centerUtm, &singleWidthOut, &singleHeightOut); else mtps_asUtm = explosionClusterAreaToMtps(areaBlWithTla, m_data.mhpCountRequest, &singleWidthOut, &singleHeightOut); if (mtps_asUtm.size() == m_data.mhpCountRequest) { m_temporaryData.mhpLength = singleHeightOut; m_temporaryData.mhpWidth = singleWidthOut; // m_temporaryData.mhpRotation = m_data.rotationAngle; // mhpRotation depends on firingOrigin m_temporaryData.mhPoints.clear(); // GUEZE-2746 // if less impacts are requested than mtps are provided: if (m_data.impactCount < mtps_asUtm.size()) mtps_asUtm.resize(m_data.impactCount); m_temporaryData.mhPoints.reserve(mtps_asUtm.size()); for (const QPointF &mtp_asUtm : qAsConst(mtps_asUtm)) { LtsCoordTrans::UtmCoordinate temp(impactPositionUtm.zone, mtp_asUtm.x(), mtp_asUtm.y()); m_temporaryData.mhPoints.emplace_back(Core::GeoPosition2D(temp.geographic().x(), temp.geographic().y())); } // for } // if (mtps_asUtm.size() == m_data.mhpCountRequest) } // if (!isVulcano()) if (!isOriginatorParticipantIdProvided()) { DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << ", no originatorParticantId -> no trajectrory"; // it's already reseted, but to make it clear: m_temporaryData.estimatedMaxFlightDurationMSec = 0; m_temporaryData.calculatedFiringAngle = 0; m_temporaryData.calculatedFiringSpeed = 0; m_temporaryData.calculatedPeakHeightAbs = 0; m_temporaryData.calculatedFlightDurationMsec = 0; return true; } // // estimate max FlightDuration: (to start the generation of trajectories) // if (isVulcano()) { // For the flightDuration more or less we have to calculate the whole curve. // So we calculate the GTL ... QString reasonGtlVulcanoOut; bool successGTL = !generateGunToTargetForm(&reasonGtlVulcanoOut).empty(); if (!successGTL || !reasonGtlVulcanoOut.isEmpty()) { ERROR_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " name= '" << m_data.name << ", generateGunToTargetForm failed" << ", reasonGtlVulcanoOut = " << reasonGtlVulcanoOut; if (reasonOut) *reasonOut = reasonGtlVulcanoOut; return false; } if (!reasonGtlVulcanoOut.isEmpty()) ERROR_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " id/name= '" << m_data.name << "'/" << m_data.id << " reasonGtlVulcanoOut = " << reasonGtlVulcanoOut; // ... and retrieve the duration: const TracjectoryData &tdRefGTL = m_trajectoryCont[-1]; const TrajectoryTimeAware &trajTimeAwareGTL = tdRefGTL.trajectoryTimeAware; const Core::GeoPositionRelativeTime &geoPosRelTimeRef = trajTimeAwareGTL.geometry().back(); m_temporaryData.calculatedFlightDurationMsec = geoPosRelTimeRef.relativeTime(); m_temporaryData.estimatedMaxFlightDurationMSec = (geoPosRelTimeRef.relativeTime() * 1.1) + 5 * 1000; // ( *1.1) + (10 *1000) m_temporaryData.calculatedFiringSpeed = TrajectoryGeneratorVulcano::defaultv0(); m_temporaryData.calculatedPeakHeightAbs = tdRefGTL.peakHeightAbs; // to avoid re-calculation, store that at m_aivGunToTargetTrajectory. // At publishAiv the new calculation will be skipped: // m_aivGunToTargetTrajectory.form(tdRefGTL.trajectoryGeometry); m_aivGunToTargetTrajectoryDirty = true; } else { m_temporaryData.calculatedFlightDurationMsec = 10.0; // Polygons::EffectClusterCalculatedFlightDuration2DMSec(m_data); if (m_isAirSupportSource) m_temporaryData.estimatedMaxFlightDurationMSec = m_temporaryData.calculatedFlightDurationMsec; else m_temporaryData.estimatedMaxFlightDurationMSec = 10.0; // Polygons::EffectClusterEstimatedMaxFlightDuration2DMSec(m_temporaryData.calculatedFlightDurationMsec); // ( *1.1) + (10 *1000) } DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << ", calculatedFlightDuration=" << m_temporaryData.calculatedFlightDurationMsec << ", m_estimatedMaxFlightDurationMSec=" << m_temporaryData.estimatedMaxFlightDurationMSec; TRACE_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " END, m_estimatedMaxFlightDurationMSec=" << m_temporaryData.estimatedMaxFlightDurationMSec; const bool ret = m_temporaryData.calculatedFlightDurationMsec > 0; if (!ret && reasonOut) *reasonOut = QStringLiteral("no MaxFlightDuration"); return ret; } void ObjectEffectCluster::abortPreStartTimer() { m_preStartTimer->stop(); DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << ", preStartTimer STOPPED"; } Core::FunctionReplyObject ObjectEffectCluster::removeObject() { Core::FunctionReplyObject ret = ObjectBaseTmpl::removeObject(); if (ret.error()) return ret; // avoid unneeded calculations: m_preStartTimer->stop(); DEBUG_LOG(m_logger) << "Impact count: " << m_data.impactCount; int remainingAmmoCount = 0; // calc for (const auto &effectorData : m_effectorData) { remainingAmmoCount += effectorData.chargeAmount - m_firedPerEfector.value(effectorData.participantId); } DEBUG_LOG(m_logger) << "remainingAmmoCount: " << remainingAmmoCount; if (remainingAmmoCount < 0) remainingAmmoCount = 0; DEBUG_LOG(m_logger) << "Charge left / remainingAmmoCount: " << remainingAmmoCount; // restore original stuff m_data.effectDuration = m_data.originalEffectDuration; m_data.effectStart = m_data.originalEffectStart; // Negative, as we free the charge if (m_data.effectSourceId != ProjectDataManagement::EffectSource::ES_ADLER && m_data.effectSourceId != ProjectDataManagement::EffectSource::ES_ADLER_STANDALONE) m_mainHandler->chargeAdministrationHandler()->claimPlannedCharge(m_data.chargeTypeId, m_data.partyId, 0 - remainingAmmoCount); return ret; } Core::FunctionReplyObject ObjectEffectCluster::checkObjectData(const Database::AesEffectCluster &data) const { // To avoid crashed / asserts etc Core::FunctionReplyObject ret; using geoType = AreaEffectSimulation::AesExplosionGeometry_def; const geoType::type curExGeoType = data.explosionGeometryId.underlying(); static const QVector ecValidGeos = {AreaEffectSimulation::AesExplosionGeometry_def::EgAimPoint, // 0 -> default AreaEffectSimulation::AesExplosionGeometry_def::EgPoint, AreaEffectSimulation::AesExplosionGeometry_def::EgArea, AreaEffectSimulation::AesExplosionGeometry_def::EgRadial, AreaEffectSimulation::AesExplosionGeometry_def::EgLine}; QString reason; if (data.name.isEmpty()) reason = QStringLiteral("Name is empty"); else if (data.impactPosition.isNull()) reason = QStringLiteral("Position is null"); else if (data.impactLength <= 0) reason = QString(QStringLiteral("ImpactLength invalid (%1)")).arg(data.impactLength); else if (data.impactWidth <= 0) reason = QString(QStringLiteral("ImpactWidth invalid (%1)")).arg(data.impactWidth); else if (data.impactCount <= 0) reason = QString(QStringLiteral("ImpactCount invalid (%1)")).arg(data.impactCount); else if (!ecValidGeos.contains(curExGeoType)) reason = QString(QStringLiteral("ExplosionGeometryType not supported yet (%1)")).arg(static_cast(curExGeoType)); else if (data.tla < 0) reason = QString(QStringLiteral("TLA invalid (%1)")).arg(data.tla); // LaserGuiding: else if (data.launchVelocity < -1) reason = QString(QStringLiteral("launchVelocity invalid (%1)")).arg(data.launchVelocity); else if ((data.launchAngleDeg < -1) || (data.launchAngleDeg > 90)) reason = QString(QStringLiteral("launchAngleDeg invalid (%1)")).arg(data.launchAngleDeg); else if ((data.dropAngle < -1) || (data.dropAngle > 90)) reason = QString(QStringLiteral("dropAngle invalid (%1)")).arg(data.dropAngle); // finally if (!reason.isEmpty()) { ret = Core::FunctionReplyObject(m_data.id, tr_lts("dds", "Effect cluster with id %1 and name '%2' invalid. (%3)")).arg(data.id).arg(data.name).arg(reason); ERROR_LOG(m_logger) << LOGFUNCTION << " " << data.id << " Error: " << ret.translated(); } return ret; } bool ObjectEffectCluster::removeObjectFromDb() { return m_mainHandler->db()->removeAesEffectCluster(m_data.id); } Core::FunctionReplyObject ObjectEffectCluster::resumePause(const EffectClusterPauseResumeRequest &req) { Core::FunctionReplyObject ret; if (req.resumeExecution()) { m_data.effectStart = partitionTime(); // resume if (!trySwitchToActivated()) return Core::FunctionReplyObject(m_data.id, tr_lts("dds", "EffectCluster is currently not paused.")); DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " RESUME" << " ... "; } else { // pause if (!trySwitchToPaused()) return Core::FunctionReplyObject(m_data.id, tr_lts("dds", "Switching EffectCluster to pause is not possible in the current state.")); DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " PAUSE" << " ... "; } if (ret.error()) ERROR_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " PAUSE" << " ... resumePause failed" << ", errorStr = " << ret.translated(); return ret; } Core::FunctionReplyObject ObjectEffectCluster::targetPositionsModify(const TargetPositionsModifyRequest &req) { static int s_counter = 0; const qint64 now = partitionTimeMSec(); const qint64 remainingMsec = m_data.effectStart.toMSecsSinceEpoch() - now; DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " START" << " TargetPositionsModifyRequest = " << req << ", s_counter = " << s_counter++ << ", current state = " << m_data.effectStateId.underlying() << ", now = " << now << ", effectStart = " << m_data.effectStart.toMSecsSinceEpoch() << ", remainingMsec = " << remainingMsec; const std::vector &positions = req.positions(); Core::FunctionReplyObject ret; if (m_data.effectStateId.underlying() == AreaEffectSimulation::AesEffectState_def::EsExpired) { ret = Core::FunctionReplyObject(m_data.id, tr_lts("dds", "Error modifying explosion cluster with id %1 since object is already expired")).arg(m_data.id); ERROR_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " Error: " << ret.str(); return ret; } // check size if (positions.empty() || (m_data.mhpCountRequest != static_cast(positions.size()))) { ret = Core::FunctionReplyObject(m_data.id, tr_lts("dds", "Error modifying explosion cluster with id %1 since size does not match")).arg(m_data.id); ERROR_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " Error: " << ret.str(); return ret; } // check isVulcano: if (!isVulcano()) { ret = Core::FunctionReplyObject(m_data.id, tr_lts("dds", "Error modifying explosion cluster with id %1 is not supported for this charge type")).arg(m_data.id); ERROR_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " Error: " << ret.str(); return ret; } // check Time: // accept targetPositionsModify only if effectStart is NOT reached: if (remainingMsec < 0) { ret = Core::FunctionReplyObject(m_data.id, tr_lts("dds", "Error modifying explosion cluster with id %1 since object is already active (it's %2 msec to late) ")) .arg(m_data.id) .arg(remainingMsec); ERROR_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " Error: " << ret.translated(); return ret; } const Core::GeoPosition &newTargetPos = req.positions().front(); const int explosionIndex = 0; TracjectoryData &tdRef = m_trajectoryCont[explosionIndex]; TrajectoryTimeAware &trajTimeAwareRef = tdRef.trajectoryTimeAware; std::vector &trajectoryTimeAwareGeometry = trajTimeAwareRef.geometry(); if (trajectoryTimeAwareGeometry.empty()) { ERROR_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " has no trajectoryTimeAwareGeometry"; ret = Core::FunctionReplyObject(m_data.id, tr_lts("dds", "Error modifying explosion cluster with id %1 since trajectory is empty")).arg(m_data.id); return ret; } // "adjust" by replacing the last GeoPosition by the new value, but leave the timepoint untouched: Core::GeoPosition &hitPos = trajectoryTimeAwareGeometry.back(); // implicit downcast hitPos = newTargetPos; // m_aiv2 CHANGES HERE // std::vector &formRef = m_aiv2.form(); // compare former and updated ImpactPos: context targetModify if (m_logger.isDebugEnabled()) { Core::GeoPosition &curImpactPosRef = formRef[explosionIndex]; LtsCoordTrans::UtmCoordinate utm1 = LtsCoordTrans::UtmCoordinate::fromGeographic(curImpactPosRef.lon(), curImpactPosRef.lat()); const QVector3D u1Vec(utm1.x, utm1.y, curImpactPosRef.z()); LtsCoordTrans::UtmCoordinate utm2 = LtsCoordTrans::UtmCoordinate::fromGeographic(newTargetPos.lon(), newTargetPos.lat(), utm1.zone); const QVector3D u2Vec(utm2.x, utm2.y, newTargetPos.z()); const QVector3D diff = u2Vec - u1Vec; DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " for explosionIndex " << explosionIndex << ", curImpactPosRef " << curImpactPosRef << ", utm1 " << utm1 << ", newTargetPos " << newTargetPos << ", utm2 " << utm2 << " the impactPos moved 2D = " << LtsCoordTrans::distance(utm1, utm2) << " the impactPos moved 3D = " << diff.length(); } Core::GeoPosition &curImpactPosRef = formRef[explosionIndex]; curImpactPosRef = newTargetPos; // Effects: std::vector &singleEffectsRef = m_effects.singleEffects(); SingleEffect &ref = singleEffectsRef[explosionIndex]; ref.position(newTargetPos); ref.zPlacement(AesZPlacement_def::ZpAbsolute); // intersection: change from draped to absolute // Trajectory publishTrajectory(explosionIndex); if (ret.error()) ERROR_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " some checks failed"; return ret; } Core::FunctionReplyObject ObjectEffectCluster::checkPreConditionsComplete() { DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " START"; QString reason; Core::FunctionReplyObject ret; if (m_data.externalId <= 0) // -> internal { if (m_data.participantId == 0) { WARNING_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << ", m_data.participantId invalid: " << m_data.participantId; reason = QString(QStringLiteral("firingParticipantId is invalid (%1)")).arg(m_data.participantId); DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " END"; return Core::FunctionReplyObject(m_data.id, reason.toStdString().c_str()); } if ((m_data.participantId > 0) && !firingOriginIsValid(m_data.firingCenterPosition, &reason)) { WARNING_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " firingOriginIsValid failed"; if (!reason.isEmpty()) { DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " END"; return Core::FunctionReplyObject(m_data.id, reason.toStdString().c_str()); } } if (!initialCalculations(&reason) || !reason.isEmpty()) { WARNING_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " initialCalculations failed"; return Core::FunctionReplyObject(m_data.id, reason.toStdString().c_str()); } } // if (m_data.externalId <= 0) // keep start of svc in mind: if (m_data.effectStateId.underlying() != AreaEffectSimulation::AesEffectState_def::EsExpired) ret = checkPreConditions(); if (ret.error()) ERROR_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " checkPreConditions failed"; return ret; } void ObjectEffectCluster::setEffectState(AesEffectState newState) { // more or less a hack for ec of HLA (m_EffectCluster.externalId() > 0) // PATRIC if (newState == AesEffectState::EsExpired || newState == AesEffectState::EsRemoved || newState == AesEffectState::EsError) { freeEffectors(); if (m_fireAutomaton) { m_automatonTimer->stop(); } } bool bExecuteFiringImpact = false; if (noTracjectoryRequired() && (m_data.effectStateId != AesEffectState::EsActivated) && (newState == AesEffectState::EsActivated)) bExecuteFiringImpact = true; ObjectBaseTmpl::setEffectState(newState); // if (newState == AesEffectState::EsActivated) // startSingleExplosionHandling(); if (newState == AesEffectState::EsCreated || newState == AesEffectState::EsError || newState == AesEffectState::EsReceived) { DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << ", setEffectState to " << newState; abortPreStartTimer(); } if (bExecuteFiringImpact) { m_temporaryData.calculatedFlightDurationMsec = 0; checkNextFire(); } m_publishDataTimer->start(); } void ObjectEffectCluster::onPublishData() { ObjectBaseTmpl::onPublishData(); if (!m_mainHandler->db()->update(m_data)) ERROR_LOG(m_logger) << "Error updating ObjectEffectCluster-data in database"; // base from first EffectCluster ddsData(m_data.id, m_data.name.toStdString(), effectState() == AesEffectState::EsRemoved, false, 0, // partitionTimeMSec(), {m_effectorData.begin().value().chargeTypeId, m_effectorData.begin().value().igniterTypeId, m_effectorData.begin().value().igniterFunctionId}, m_data.effectDuration, // pause/resume: remainDuration toOptional(toMSec(m_data.effectStart)), m_data.effectStateId, m_data.effectSourceId, toOptional(m_data.partyId), toOptional(m_data.participantId), m_data.effectMarkingInTa, m_data.externalId, m_data.isCentral, m_data.properties.toStdString(), m_data.element.toStdString(), Area(Core::GeoPosition2D(m_data.impactPosition.x(), m_data.impactPosition.y()), m_data.impactWidth, m_data.impactLength, m_data.rotationAngle), m_data.explosionGeometryId, m_data.tla, toEffectorVec(m_effectorData.values()), m_data.remainExplosionNumber, // is more _NEXT_ ExplosionNumber toMSec(m_data.fireRequestTime), m_data.firePurposeId, // m_data.firePurpose.toStdString(), toCoreGeoPosition(m_data.firingCenterPosition), toMSec(m_data.originalEffectStart), m_data.originalEffectDuration, m_temporaryData.isObjectModifiable, // dds::core::optional(), // dds::core::optional(), m_data.esmParticipantIds(), static_cast(m_data.effectorState), m_temporaryData.annotationString); // publish only on real changes: if (m_EffectCluster != ddsData) { m_EffectCluster = ddsData; ddsData.sampleTime(partitionTimeMSec()); // publish sampleTime, but do not store m_mainHandler->dds()->dts()->publish(ddsData); } publishAiv(); } void ObjectEffectCluster::onDetonationLocationReply(const DetonationLocationReply &reply) { TRACE_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " START"; // int32_t aesItemId, // aesItemId // const std::string& annotationString, // const std::vector< std::string >& errorParams, // int32_t explosionIndex, // explosionIndex // const Core::GeoPosition& position); if (reply.id() != m_data.id) // aesItemId { DEBUG_LOG(m_logger) << "ID mismatch: " << reply.id() << " != " << m_data.id; return; } // do we have the initial geometry? const int explosionIndex = reply.index(); if (!m_trajectoryCont.contains(explosionIndex)) { DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << "unknown explosionIndex " << ", reply.index() = " << explosionIndex; return; } const Core::GeoPosition &replyPos = reply.position(); if ((replyPos.lon() < 0.00001) || (replyPos.lat() < 0.00001)) { // no intersection DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " got DetonationLocationReply" << " : no intersection - reply.position is: " << ", reply.index() = " << explosionIndex << ", reply.position() " << replyPos; } else { DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " got DetonationLocationReply" << " : INTERSECTION - reply.position is: " << ", reply.index() = " << explosionIndex << ", reply.position() " << replyPos; adjustTrajectoryToDetonationLocation(explosionIndex, replyPos, reply.buildingId()); } // publish new triggered explosions data here // first new positions... if (m_aiv2HasChanged) { DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " Publishing AesItemView (AtEffectClusterSingleExplosions) for TriggeredExplosions, explosionIndex " << explosionIndex; m_aiv2.sampleTime(partitionTimeMSec()); // // m_mainHandler->dds()->dts()->publish(m_aiv2); // does anyone needs this topic??? m_aiv2HasChanged = false; emit newAiv(m_aiv2); } else DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " Publishing AesItemView (AtEffectClusterSingleExplosions) for TriggeredExplosions SKIPPED, explosionIndex " << explosionIndex; publishTrajectory(explosionIndex); TRACE_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " END"; } void ObjectEffectCluster::adjustTrajectoryToDetonationLocation(int explosionIndex, const Core::GeoPosition &intersectionLocation, const int buildingId) { TRACE_LOG(m_logger) << "adjustTrajectoryToDetonationLocation " << m_data.id << " START"; // do we have the initial geometry? if (!m_trajectoryCont.contains(explosionIndex)) { ERROR_LOG(m_logger) << "adjustTrajectoryToDetonationLocation " << m_data.id << "unknown explosionIndex " << ", reply.index() = " << explosionIndex; return; } TracjectoryData &tdRef = m_trajectoryCont[explosionIndex]; tdRef.buildingId = buildingId; std::vector &trajectoryGeometry = tdRef.trajectoryGeometry; if (trajectoryGeometry.empty()) { ERROR_LOG(m_logger) << "adjustTrajectoryToDetonationLocation " << m_data.id << " trajectory is empty"; return; } DEBUG_LOG(m_logger) << "adjustTrajectoryToDetonationLocation " << m_data.id << "VOR : m_trajectory.size()=" << trajectoryGeometry.size() << " back: " << trajectoryGeometry.back(); const Core::GeoPosition &trajBack = trajectoryGeometry.back(); if (trajectoryGeometry.back() == intersectionLocation) // no adjustment necessary { DEBUG_LOG(m_logger) << "Detonation/Intersection location coincides with last point of trajectory for ID: " << m_data.id; return; } const Core::GeoPosition &trajFront = trajectoryGeometry.front(); const double distToOriginSquared = LtsCoordTrans::squareDistance(trajFront.lon(), trajFront.lat(), intersectionLocation.lon(), intersectionLocation.lat()); ProjectDataManagement::IgniterFunctionBase ignFunctionBase = m_mainHandler->dds()->pdmDtc()->getChargeTypeIgniterTypeIgniterFunction(m_data.chargeTypeId, 0, 0).igniterFunctionBaseId(); if ((ignFunctionBase == ProjectDataManagement::IgniterFunctionBase::IfbProximity) || (ignFunctionBase == ProjectDataManagement::IgniterFunctionBase::IfbImpact)) { const double distTrajectorySquared = LtsCoordTrans::squareDistance(trajFront.lon(), trajFront.lat(), trajBack.lon(), trajBack.lat()); if (distToOriginSquared > distTrajectorySquared) { ERROR_LOG(m_logger) << "adjustTrajectoryToDetonationLocation " << m_data.id << "Detonation location is outside of trajectory for ID: " << m_data.id << ", explosionIndex=" << explosionIndex; return; } // if } bool bAdjusted = false; // reverse iteration; const Core::GeoPosition &trajectoryOrigin_geo = trajFront; for (unsigned int i = trajectoryGeometry.size() - 1; i > 0; --i) { const Core::GeoPosition &curItem_geo = trajectoryGeometry[i - 1]; const double distStartSegmentSquared = LtsCoordTrans::squareDistance(curItem_geo.lon(), curItem_geo.lat(), trajectoryOrigin_geo.lon(), trajectoryOrigin_geo.lat()); // remove... if (distStartSegmentSquared > distToOriginSquared) { trajectoryGeometry.pop_back(); continue; } // if // ... and set new end/back: trajectoryGeometry.pop_back(); trajectoryGeometry.push_back(intersectionLocation); bAdjusted = true; } // for if (bAdjusted) { // 'adjust' the according SingleExlosions position!? // initially form has no zValue, but we replace the ideal impact with the intersectionLocation (with zValue) // // m_aiv2 CHANGES HERE // std::vector &formRef = m_aiv2.form(); Core::GeoPosition &curImpactPosRef = formRef[explosionIndex]; // compare former and updated ImpactPos: context intersection if (m_logger.isDebugEnabled()) { LtsCoordTrans::UtmCoordinate utm1 = LtsCoordTrans::UtmCoordinate::fromGeographic(curImpactPosRef.lon(), curImpactPosRef.lat()); const QVector3D u1Vec(utm1.x, utm1.y, curImpactPosRef.z()); LtsCoordTrans::UtmCoordinate utm2 = LtsCoordTrans::UtmCoordinate::fromGeographic(intersectionLocation.lon(), intersectionLocation.lat(), utm1.zone); const QVector3D u2Vec(utm2.x, utm2.y, intersectionLocation.z()); const QVector3D diff = u2Vec - u1Vec; DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " for explosionIndex " << explosionIndex << ", curImpactPosRef " << curImpactPosRef << ", utm1 " << utm1 << ", intersectionLocation " << intersectionLocation << ", utm2 " << utm2 << " the impactPos moved 2D = " << LtsCoordTrans::distance(utm1, utm2) << " the impactPos moved 3D = " << diff.length(); } curImpactPosRef = intersectionLocation; m_aiv2HasChanged = true; // Effects: std::vector &singleEffectsRef = m_effects.singleEffects(); SingleEffect &ref = singleEffectsRef[explosionIndex]; // ref.effectState(AesSingleEffectState_def::SesScheduled); ref.position(intersectionLocation); ref.zPlacement(AesZPlacement_def::ZpAbsolute); // intersection: change from draped to relative // m_effectsHaveChanged = true; if (explosionIndex == m_data.impactCount - 1) DEBUG_LOG(m_logger) << "adjustTrajectoryToDetonationLocation " << m_data.id << ", update SingleExlosion: " << ", explosionIndex=" << explosionIndex << ", intersectionLocation=" << intersectionLocation; else DEBUG_LOG(m_logger) << "adjustTrajectoryToDetonationLocation " << m_data.id << ", update SingleExlosion: " << ", explosionIndex=" << explosionIndex << ", intersectionLocation=" << intersectionLocation; } DEBUG_LOG(m_logger) << "adjustTrajectoryToDetonationLocation " << m_data.id << ", Trajectory bAdjusted: " << bAdjusted << ", m_trajectory.size()=" << trajectoryGeometry.size() << " back: " << trajectoryGeometry.back(); TRACE_LOG(m_logger) << "adjustTrajectoryToDetonationLocation, " << m_data.id << " END"; } void ObjectEffectCluster::publishAiv() { QPolygonF poly; // [GUEZE-2487] PointGeometry: // Move circle / form to real impact: const bool bIsPoint = isPointSimilar(m_data.explosionGeometryId); if (bIsPoint && (m_lastSentTriggeredExplosionNumber == 1)) // 'move' only AFTER impact { // on svcRestart, m_aiv2 may not been set. To avoid crash: const std::vector &formRef = m_aiv2.form(); DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " since isPointSimilar==true form has to be updated" << ", m_data.effectStateId = " << m_data.effectStateId << ", m_data.impactWidth = " << m_data.impactWidth << ", m_lastSentTriggeredExplosionNumber = " << m_lastSentTriggeredExplosionNumber << ", formRef.size() = " << formRef.size(); QPointF centerGeo; // on svcRestart, m_aiv2 may not been set. To avoid crash: if (formRef.empty()) centerGeo = AreaEffectSimulation::Polygons::bottomLeft2CenterPoint(m_data.impactPosition, m_data.impactWidth, m_data.impactLength, m_data.rotationAngle); else { const Core::GeoPosition &impactPosition_geo3D = formRef[0]; centerGeo = QPointF(impactPosition_geo3D.lon(), impactPosition_geo3D.lat()); } // ... and now take zog into account: double radius = m_data.impactWidth / 2.0; if (radius < 1) radius = 1; poly = Polygons::circle(centerGeo, radius + m_data.tla); } else poly = Polygons::effectClusterArea(m_data); const QRectF bounds(poly.boundingRect()); // // Major // AesItemView aiv(m_aiv); aiv.aesItemId(m_data.id); aiv.chargeStruct({m_data.chargeTypeId, 0, 0}); aiv.effectStart(toMSec(m_data.effectStart)); ddsVecFromPoly(poly, aiv.form()); aiv.boundingBoxBottomLeft(Core::GeoPosition2D(bounds.topLeft().x(), bounds.topLeft().y())); // QRectF's bigger values are at bottom aiv.boundingBoxTopRight(Core::GeoPosition2D(bounds.bottomRight().x(), bounds.bottomRight().y())); aiv.aesTypeId(AesType::AtEffectCluster); aiv.effectStateId(effectState()); aiv.id(1000 * m_data.id + AesType::AtEffectCluster); if (m_data.partyId != 0) aiv.partyId(m_data.partyId); // ObjectBaseTmpl::checkIsCentral honors pdm->isLaserGuidable // and therefore isCentral is set to true; ) checkIsCentralAndPublish(aiv.form(), m_aiv.form(), m_data.isCentral); // // SingleExplosions // { AesItemView aiv2(m_aiv2); aiv2.aesItemId(m_data.id); aiv2.chargeStruct({m_data.chargeTypeId, 0, 0}); aiv2.effectStart(toMSec(m_data.effectStart)); // calculate just one time. // maybe intersections of trajectories with building will 'adjust' some impact positions later. if (aiv2.form().empty()) { if (isVulcano() || (m_data.externalId > 0)) // m_data.externalId > 0 -> HLA / SingleFire / PointGeometry { const QPointF &impactPointGeo = AreaEffectSimulation::Polygons::bottomLeft2CenterPoint(m_data.impactPosition, m_data.impactWidth, m_data.impactLength, m_data.rotationAngle); ddsVecFromPoly(QVector() << impactPointGeo, aiv2.form()); } else { const QVector &singleExplosions = Polygons::effectClusterSingleExplosionsGAUSS_Mtp(m_data, m_effectorData); ddsVecFromPoly(singleExplosions, aiv2.form()); } } aiv2.boundingBoxBottomLeft(aiv.boundingBoxBottomLeft()); aiv2.boundingBoxTopRight(aiv.boundingBoxTopRight()); aiv2.aesTypeId(AesType::AtEffectClusterSingleExplosions); aiv2.id(1000 * m_data.id + AesType::AtEffectClusterSingleExplosions); aiv2.effectStateId(effectState()); if (m_data.partyId != 0) aiv2.partyId(m_data.partyId); // // m_aiv2 CHANGES HERE // m_aiv2 = aiv2; // if (aiv2 != m_aiv2) // { // m_aiv2 = aiv2; // aiv2.sampleTime(partitionTimeMSec()); // publish sampleTime, but do not store // m_mainHandler->dds()->dts()->publish(aiv2); // emit newAiv(aiv2); // } m_effects.id(1000 * m_data.id + AesType::AtEffectClusterSingleExplosions); m_effects.aesItemId(m_data.id); m_effects.aesTypeId(AesType::AtEffectClusterSingleExplosions); std::vector &singleEffectsRef = m_effects.singleEffects(); if (singleEffectsRef.empty()) { if (isVulcano() || (m_data.externalId > 0)) // m_data.externalId > 0 -> HLA / SingleFire / PointGeometry { singleEffectsRef.resize(1); SingleEffect &seRef = singleEffectsRef.front(); seRef.singleEffectState(AesSingleEffectState_def::SesScheduled); const QPointF &impactPointGeo = AreaEffectSimulation::Polygons::bottomLeft2CenterPoint(m_data.impactPosition, m_data.impactWidth, m_data.impactLength, m_data.rotationAngle); // seRef.position(Core::GeoPosition(impactPointGeo.x(), impactPointGeo.y(), 0.0)); const double elevation = getElevationFor(impactPointGeo, QStringLiteral("getElevationForSingleImpactVulcano")); seRef.position(Core::GeoPosition(impactPointGeo.x(), impactPointGeo.y(), elevation)); seRef.zPlacement(AesZPlacement_def::ZpDraped); } else { const QVector &singleExplosions = Polygons::effectClusterSingleExplosionsGAUSS_Mtp(m_data, m_effectorData); singleEffectsRef.reserve(singleExplosions.size()); for (const QPointF &singleExplosion : qAsConst(singleExplosions)) { SingleEffect se; se.singleEffectState(AesSingleEffectState_def::SesScheduled); // se.position(Core::GeoPosition(singleExplosion.x(), singleExplosion.y(), 0.0)); const double elevation = getElevationFor(singleExplosion, QStringLiteral("getElevationForSingleEffect")); se.position(Core::GeoPosition(singleExplosion.x(), singleExplosion.y(), elevation)); se.zPlacement(AesZPlacement_def::ZpDraped); // // se.effectTime(); singleEffectsRef.push_back(se); } // for } // if (isVulcano()) } } /* else { auto dts = m_mainHandler->dds()->dts(); dts->removeAesItemViewByKey(1000 * m_data.id + AesType::AtEffectClusterSingleExplosions); // up to s_maxTrajectoriesPerEffectCluster are supported. One sample per trajectory. const bool errorMsgIfSampleDoesNotExists = false; dts->removeEffectsByKey(1000 * m_data.id + AesType::AtEffectClusterSingleExplosions, errorMsgIfSampleDoesNotExists); } */ // // GunToTargetTrajectory // // EsUnknown = 0, // EsError = 255, // EsExpired = 1, // EsRemoved = 2, // EsCreated = 4, // EsPrepared = 5, // EsActivated = 6, // EsPaused = 7 // AtEffectClusterGunToTargetLine is suppressed at map display, when main object changes to EsActivated // So it does not have to be changed/published anymore. // Especially helpful while restarting scvAes! static QVector relevantEffStates = {AesEffectState_def::EsReceived, AesEffectState_def::EsCreated, AesEffectState_def::EsPrepared, AesEffectState_def::EsFired, AesEffectState_def::EsActivated, // Jira [LS-647] gefordert dauerhaft sichtbar AesEffectState_def::EsExpired}; // Jira [LS-647] gefordert dauerhaft sichtbar DEBUG_LOG(m_logger) << " AtEffectClusterGunToTargetLine RELEVANT " << relevantEffStates.contains(effectState().underlying()) << ", id " << m_data.id; for (const auto &effector : m_effectorData) { if (isOriginatorParticipantIdProvided() && // no Originator -> no curve relevantEffStates.contains(effectState().underlying())) { AesItemView aiv3(m_aivGunToTargetTrajectoryMap.value(effector.participantId)); aiv3.aesItemId(m_data.id); aiv3.chargeStruct({effector.chargeTypeId, 0, 0}); // aiv3.effectStart(0); if (aiv3.form().empty()) { // E01562-294: Falsche Darstellung Flugkurve SMArt const auto &trajectoryRelativeToOriginator = TrajectoryRelative::fromString(effector.trajectory); const auto &trajectoryRelativeToOriginatorSingleDueToSMArt = reduceSMArtTrajectoryToSingle(trajectoryRelativeToOriginator); if (trajectoryRelativeToOriginator.size() != trajectoryRelativeToOriginatorSingleDueToSMArt.size()) DEBUG_LOG(m_logger) << "generateGunToTargetForm for id: " << " " << m_data.id << ": there is a step backward in time, reduce input used as GunToTargetLine."; const std::vector &form = ObjectEffectCluster::generateGunToTargetForm(trajectoryRelativeToOriginatorSingleDueToSMArt, effector.participantId); aiv3.form(form); if (form.size() > 2) { QPolygonF polyGTL; polyGTL.reserve(2); polyGTL.push_back(QPointF(form.front().lon(), form.front().lat())); polyGTL.push_back(QPointF(form.back().lon(), form.back().lat())); const QRectF boundsGTL(polyGTL.boundingRect()); aiv3.boundingBoxBottomLeft(Core::GeoPosition2D(boundsGTL.topLeft().x(), boundsGTL.topLeft().y())); // QRectF's bigger values are at bottom aiv3.boundingBoxTopRight(Core::GeoPosition2D(boundsGTL.bottomRight().x(), boundsGTL.bottomRight().y())); } } aiv3.aesTypeId(AesType::AtEffectClusterGunToTargetLine); aiv3.id(1000 * m_data.id * effector.participantId + AesType::AtEffectClusterGunToTargetLine); // aiv3.effectStateId(effectState()); if (m_data.partyId != 0) aiv3.partyId(m_data.partyId); m_aivGunToTargetTrajectoryMap.insert(effector.participantId, aiv3); aiv3.sampleTime(partitionTimeMSec()); // publish sampleTime, but do not store m_mainHandler->dds()->dts()->publish(aiv3); emit newAiv(aiv3); } else { auto dts = m_mainHandler->dds()->dts(); const bool errorMsgIfSampleDoesNotExists = false; dts->removeAesItemViewByKey(1000 * m_data.id * effector.participantId + AesType::AtEffectClusterGunToTargetLine, errorMsgIfSampleDoesNotExists); } } if (aiv != m_aiv) { m_aiv = aiv; aiv.sampleTime(partitionTimeMSec()); // publish sampleTime, but do not store m_mainHandler->dds()->dts()->publish(aiv); emit newAiv(aiv); } } bool ObjectEffectCluster::intersectWithCircle(double lonRad, double latRad, int radius) { LtsCoordTrans::UtmCoordinate bottomLeftUtm = LtsCoordTrans::UtmCoordinate::fromGeographic(m_EffectCluster.area().cornerBottomLeft().lon(), m_EffectCluster.area().cornerBottomLeft().lat()); LtsCoordTrans::UtmCoordinate circleCenterUtm = LtsCoordTrans::UtmCoordinate::fromGeographic(lonRad, latRad, bottomLeftUtm.zone); const double xMin = bottomLeftUtm.x; const double yMin = bottomLeftUtm.y; const double xMax = bottomLeftUtm.x + m_EffectCluster.area().width(); const double yMax = bottomLeftUtm.y + m_EffectCluster.area().height(); const double farthestX = (std::abs(circleCenterUtm.x - xMin) > std::abs(circleCenterUtm.x - xMax)) ? xMin : xMax; const double farthestY = (std::abs(circleCenterUtm.y - yMin) > std::abs(circleCenterUtm.y - yMax)) ? yMin : yMax; const double distanceX = circleCenterUtm.x - farthestX; const double distanceY = circleCenterUtm.y - farthestY; const double distanceSquared = (distanceX * distanceX) + (distanceY * distanceY); const double radiusSquard = (radius * radius); DEBUG_LOG(m_logger) << "Farthest Point Check:" << "\n - Rect Bounds: x[" << xMin << " to " << xMax << "], y[" << yMin << " to " << yMax << "]" << "\n - Circle Center: (" << circleCenterUtm.x << ", " << circleCenterUtm.y << ")" << "\n - Farthest Point: (" << farthestX << ", " << farthestY << ")" << "\n - Dist Squared: " << distanceSquared << "\n - Radius: " << radius << "\n - Radius Squared: " << radiusSquard; return distanceSquared <= radiusSquard; ; } void ObjectEffectCluster::markAsExpired(bool fireLoadedRound) { DEBUG_LOG(m_logger) << "Got Mark as Expired with fireLoadedRound " << fireLoadedRound; // delete timer just in case if (m_fireHoldWaitforLateFire) { m_fireHoldWaitforLateFire->stop(); delete m_fireHoldWaitforLateFire; m_fireHoldWaitforLateFire = nullptr; } if (!fireLoadedRound) { setEffectState(AreaEffectSimulation::AesEffectState::EsExpired); } else { // We will wait for fire events from the SAA and then send confirmation reports // as of now: the effect cluster will be set to expired after the timeout // (like the current implementation of markAsExpired without fireLoadedRound!) // but the charge effect will be probably still midair, // resulting in a visual mismatch of charge effects and the Effectcluster's EffectState // Idea: changing the "m_lookoutChargeEffectEnd" for the last round DEBUG_LOG(m_logger) << "Need To wait as it is fireloadedround=" << fireLoadedRound; m_fireHoldWaitforLateFire = new QTimer(this); m_fireHoldWaitforLateFire->setSingleShot(true); m_fireHoldWaitforLateFire->setInterval(7 * 1000); connect(m_fireHoldWaitforLateFire, &QTimer::timeout, this, [this]() { DEBUG_LOG(m_logger) << "TimerTimeout we will now send Confirmation for " << m_data.id; // send confirmation for all Effectors then mark as expired (with false) for (const auto effector : qAsConst(m_effectorData)) { onSendConfirmationReportToTab(effector.participantId); } // give them a little time to set it mark as expired QTimer::singleShot(1000, [this]() { markAsExpired(false); }); }); // Start it now m_fireHoldWaitforLateFire->start(); } } void ObjectEffectCluster::removeTopics() { // Unfortunately removeTopics() will generate about 1000 ERROR_... // Who knows a better way? const auto &dts = m_mainHandler->dds()->dts(); const bool errorMsgIfSampleDoesNotExists = false; dts->removeEffectClusterByKey(m_data.id, errorMsgIfSampleDoesNotExists); dts->removeAesItemViewByKey(1000 * m_data.id + AesType::AtEffectCluster, errorMsgIfSampleDoesNotExists); dts->removeAesItemViewByKey(1000 * m_data.id + AesType::AtEffectClusterSingleExplosions, errorMsgIfSampleDoesNotExists); dts->removeAesItemViewByKey(1000 * m_data.id + AesType::AtEffectClusterGunToTargetLine, errorMsgIfSampleDoesNotExists); // ti.id(1000 * m_data.id + AesType::AtEffectClusterSingleExplosions); dts->removeTriggeredImpactsByKey(1000 * m_data.id + AesType::AtEffectClusterSingleExplosions, errorMsgIfSampleDoesNotExists); // up to s_maxTrajectoriesPerEffectCluster are supported. One sample per trajectory. for (int explosionIndex = 0; explosionIndex < AreaEffectSimulation::s_maxTrajectoriesPerExplosionCluster; ++explosionIndex) dts->removeAesItemViewByKey(1000 * m_data.id + 100 + explosionIndex, errorMsgIfSampleDoesNotExists); // Effects / Impacts dts->removeEffectsByKey(1000 * m_data.id + AesType::AtEffectClusterSingleExplosions, errorMsgIfSampleDoesNotExists); emit removeAiv(m_data.id); } //------------------------------ // Maybe no longer needed //------------------------------ Core::FunctionReplyObject ObjectEffectCluster::checkPreConditions() { // setupParticipantOrigin, initialCalculations should be executed. QString reason; DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " START"; // // check time // m_preStartTimer->stop(); if (!activationTime().isNull() && isOriginatorParticipantIdProvided()) // no originatorParticipantId provided, so no trajectory is intended { m_preStartTimer->stop(); const qint64 timeUntilFireMSec = activationTime().toMSecsSinceEpoch() - m_temporaryData.estimatedMaxFlightDurationMSec - partitionTimeMSec(); // to support 'late' / 'immediate' requests and avoid 'Error, not enough time for estimated flight duration' // following may work: // if (timeUntilFireMSec < 0) // timeUntilFireMSec = 0; if (timeUntilFireMSec < 0) { ERROR_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " Error, not enough time for estimated flight duration" << " estimatedMaxFlightDurationMSec=" << m_temporaryData.estimatedMaxFlightDurationMSec << " (timeUntilFireMSec=" << timeUntilFireMSec << " < 0 "; reason = QString(QStringLiteral("Not enough time for estimated flight duration (timeUntilFireMSec=%1).")).arg(timeUntilFireMSec); } else { DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " preStartTimer->setInterval, timeUntilFireMSec = " << timeUntilFireMSec; m_preStartTimer->setInterval(timeUntilFireMSec); // m_preStartTimer->setInterval(5000); m_preStartTimer->start(); } // slot: checkNextFire } if (!reason.isEmpty()) { DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " END"; return Core::FunctionReplyObject(m_data.id, reason.toStdString().c_str()); } // // check GunToTargetForm (generates trajectory to center of area.) // It will use flightDuration or peakHeight as provided // if (isOriginatorParticipantIdProvided()) { QString reasonGtlOut; m_temporaryData.gunToTargetLine = ObjectEffectCluster::generateGunToTargetForm(&reasonGtlOut); if (m_temporaryData.gunToTargetLine.size() < 2) reason = QString(QStringLiteral("GenerateGunToTargetLine (%1)")).arg(reasonGtlOut); if (!reason.isEmpty()) { DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " END"; return Core::FunctionReplyObject(m_data.id, reason.toStdString().c_str()); } } return Core::FunctionReplyObject(m_data.id); } // An incomming Trajectory from the SAA will be added to the corresponding effector // and set the effect start in case of TTF Core::FunctionReplyObject ObjectEffectCluster::trajectoryAdlerModify(const TrajectoryAdlerModifyRequest &req) { Core::FunctionReplyObject ret; DEBUG_LOG(m_logger) << "ObjectEffectCluster::TrajectoryAdlerModify"; DEBUG_LOG(m_logger) << "position vector (geometry) size= " << req.geometry().size(); DEBUG_LOG(m_logger) << "explosion Index = " << req.explosionIndex(); int firstEffector = {std::numeric_limits::max()}; for (const auto &effectorData : m_effectorData) { if (effectorData.id < firstEffector) firstEffector = effectorData.id; } // find first effector auto effectorData = m_effectorData.value(req.explosionIndex()); if (effectorData.id) { DEBUG_LOG(m_logger) << "Found Effector for index " << effectorData.participantId; effectorData.flightTime = req.geometry().at(req.geometry().size() - 1).relativeTime(); if (req.flightTime() > 0.0) effectorData.flightTime = req.flightTime(); if (req.peakAltitude() > 0.0) effectorData.peakHeight = req.peakAltitude(); effectorData.trajectory = QString::fromStdString(TrajectoryRelative::toString(req.geometry())); if (req.mhpScatterLenght() > 0.0) effectorData.mhpScatterLenght = req.mhpScatterLenght(); else effectorData.mhpScatterLenght = Settings::getScatterLenghtDefault(); if (req.mhpScatterLengthDetonationHeight() > 0.0) effectorData.mhpScatterLengthDetonationHeight = req.mhpScatterLengthDetonationHeight(); if (req.mhpScatterWidth() > 0.0) effectorData.mhpScatterWidth = req.mhpScatterWidth(); else effectorData.mhpScatterWidth = Settings::getScatterWidthDefault(); if (req.mhpScatterWidthDetonationHeight() > 0.0) effectorData.mhpScatterWidthDetonationHeight = req.mhpScatterWidthDetonationHeight(); m_effectorData.insert(effectorData.participantId, effectorData); // updateEffector via effectorHandler updateEffector(effectorData.participantId, effectorData); if (m_fireAutomaton && m_data.effectDuration != DEFAULT_EFFECT_DURATION) { DEBUG_LOG(m_logger) << "Update Init after new trajectory Fire Automaton with effect duration: " << m_data.effectDuration; // use duration m_fireAutomaton->init(m_data.effectDuration, FireAutomaton::TimeType::Duration, m_data.impactCount, m_effectorData.keys().toVector(), QStringLiteral("next*")); } // update startTime if needed if (m_data.effectStart.isNull() || effectorData.id == firstEffector) { const QJsonObject probObject = QJsonDocument::fromJson(QByteArray::fromStdString(m_data.properties.toStdString())).object(); if (probObject.contains(AdlerJson::JsonKeys::ABFEUERUNGSZEIT())) { const double ttf_time = probObject[AdlerJson::JsonKeys::ABFEUERUNGSZEIT()].toDouble(); m_data.effectStart = QDateTime::fromMSecsSinceEpoch((ttf_time * 1000) + effectorData.flightTime); } } onPublishData(); // remove aiv to force new generation on publish auto aiv = m_aivGunToTargetTrajectoryMap.value(effectorData.participantId); if (aiv.form().size()) { aiv.form().clear(); m_aivGunToTargetTrajectoryMap.insert(effectorData.participantId, aiv); } publishAiv(); } return ret; } Core::FunctionReplyObject ObjectEffectCluster::effectClusterModifyActor(const EffectClusterModifyActorRequest &req) { DEBUG_LOG(m_logger) << LOGFUNCTION << "-----> req: " << req; Core::FunctionReplyObject ret; m_data.esmParticipantIds = LtsUtils::IntArray(req.esmParticipantIds()); onPublishData(); return ret; } Core::FunctionReplyObject ObjectEffectCluster::setupFiringOrigin() { TRACE_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " START"; QString reason; LtsUtils::QPoint3D geoPosOut; if (!getFiringUnitOrigin(m_data.participantId, geoPosOut, &reason)) { ERROR_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " getFiringOrigin failed for id " << m_data.participantId << ", reason: '" << reason << "'"; if (!reason.isEmpty()) { DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " END"; return Core::FunctionReplyObject(m_data.id, reason.toStdString().c_str()); } } m_data.firingCenterPosition = geoPosOut; TRACE_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " END"; return Core::FunctionReplyObject(); } void ObjectEffectCluster::checkNextFire() { DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " XXXXXXXXXXXXXXXXXXXX checkNextFire"; // For special purpose (db guez_241121) // if (m_data.name.startsWith(QStringLiteral("SAAB"))) // return; if (m_data.effectStateId == AreaEffectSimulation::AesEffectState_def::EsCreated) { DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " OBJECT IS EsCreated, RETURN;"; return; } m_preStartTimer->stop(); // object should NOT be modified anymore: if (m_temporaryData.isObjectModifiable) DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " OBJECT IS NOT Modifiable ANYMORE"; m_temporaryData.isObjectModifiable = false; onPublishData(); if (isVulcano()) { // copy data from GTL: // const TracjectoryData tdRefGTL = m_trajectoryCont[-1]; // TracjectoryData &tdRefDest = m_trajectoryCont[0]; // // // tdRefDest.index // tdRefDest.peakHeightAbs = tdRefGTL.peakHeightAbs; // tdRefDest.trajectoryGeometry = tdRefGTL.trajectoryGeometry; // tdRefDest.trajectoryTimeAware = tdRefGTL.trajectoryTimeAware; const TrajectoryGeneratorVulcano::Options opts = TrajectoryGeneratorVulcano::OptRasterApoTap | TrajectoryGeneratorVulcano::OptIncludeIdealHit // | TrajectoryGeneratorVulcano::OptRasterTapHit ; QString reasonOutCheckNextFire; generateTrajectoryVulcanoSpecific(1, opts, &reasonOutCheckNextFire); // 1 == explosionNumber if (!reasonOutCheckNextFire.isEmpty()) ERROR_LOG(m_logger) << " " << m_data.id << " reasonOutCheckNextFire=" << reasonOutCheckNextFire; publishTrajectory(0); } else { // real work: QElapsedTimer elapsedTimer; elapsedTimer.start(); // const int impactCount = m_data.impactCount; // for (int explosionNumber=1; explosionNumber<=impactCount; ++explosionNumber) // explosionNumber start with '1' // generateTrajectory(explosionNumber); // DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " generateTrajectory for all (" << impactCount << ")" // << " took " << elapsedTimer.elapsed() << " [ms]"; } } void ObjectEffectCluster::checkAutomaton() { // ERROR_LOG(m_logger) << "CHECK AUTOMATON"; if (!m_fireAutomaton) return; if (m_effectorData.empty()) return; if (m_fireAutomaton->getState() == FireAutomaton::State::Running) m_automatonTimer->stop(); int flightduration = (int)m_effectorData.begin().value().flightTime; // ERROR_LOG(m_logger) << "CHECK AUTOMATON2 "<< flightduration; // ERROR_LOG(m_logger) << "CHECK AUTOMATON3 "<< flightduration; if (partitionTime() >= m_data.effectStart.addMSecs(-flightduration)) { DEBUG_LOG(m_logger) << "START AUTOMATON fltime=" << flightduration; m_automatonTimer->stop(); m_fireAutomaton->stop(); m_fireAutomaton->start(); } } void ObjectEffectCluster::onAutomatonFinished() { DEBUG_LOG(m_logger) << "END AUTOMATON"; m_fireAutomaton->stop(); m_fireAutomaton->deleteLater(); m_fireAutomaton = nullptr; } void ObjectEffectCluster::onEffectorNotReadyToFire(int participantId, int aesItemId) { if (aesItemId != m_data.id) return; Q_UNUSED(participantId) } void ObjectEffectCluster::onEffectorReadyToFire(int participantId, int aesItemId) { if (aesItemId != m_data.id) { return; } m_readytoFire.insert(participantId); if (m_readytoFire.size() == m_effectorData.count() && m_data.effectorState == AesEffectorState::EsReadyToFire) onSendReadinessToTab(); } void ObjectEffectCluster::onEffectorReceivedOrder(int participantId, int aesItemId) { if (aesItemId != m_data.id) { return; } Q_UNUSED(participantId) } void ObjectEffectCluster::onEffectorReportedFired(int participantId, int aesItemId) { DEBUG_LOG(m_logger) << "onEffectorReportedFired! 1"; if (aesItemId != m_data.id) return; if (m_data.effectSourceId != ProjectDataManagement::EffectSource::ES_ADLER) return; const QJsonObject probObject = QJsonDocument::fromJson(QByteArray::fromStdString(m_data.properties.toStdString())).object(); FiringType firingType = FiringType::FireNow; if (probObject.contains(AdlerJson::JsonKeys::ABFEUERUNGSART())) firingType = static_cast(probObject[AdlerJson::JsonKeys::ABFEUERUNGSART()].toInt()); // We always send the first firedReport regardless of the Firing Type bool firstTime = !m_firstFireSend.contains(participantId); // Only send FireRediness after the initial one (at prep state) when we are in Mode // Fire on Command (K) DEBUG_LOG(m_logger) << "onEffectorReportedFired! 2 first time " << firstTime << " firing type " << firingType; if (!firstTime && firingType != FiringType::FireOnCommand) return; const QString reference = probObject[AdlerJson::JsonKeys::FEUERKAMPFREFERENZ()].toString(); TrainingAreaBase::SendExFiredReportRequest request; request.reference() = reference.toStdString(); request.partyId() = m_data.partyId; request.participantId() = participantId; request.identifier() = m_mainHandler->dds()->ppmDtc()->participant(participantId).name(); request.unitIdentifier() = m_mainHandler->dds()->ppmDtc()->participant(m_data.participantId).name(); m_mainHandler->dds()->tabFtc()->sendExFiredReportRequest(request, this); m_firstFireSend.insert(participantId); } void ObjectEffectCluster::onEffectorFired(int participantId, int aesItemId, const QVector &chargeEffectIds) { // NOTICE_LOG(m_logger) << "onEffectorFired" // << ", participantId=" << participantId << ", aesItemId=" << aesItemId << ", chargeEffectId=" << chargeEffectId; if (aesItemId != m_data.id) { DEBUG_LOG(m_logger) << "Got Effector Fired but not for our id: is " << aesItemId << " should be " << m_data.id; return; } // Fist append to shoot list, then look it it's the last shot and mark as last DEBUG_LOG(m_logger) << " effector should " << m_effectorData.value(participantId).chargeAmount << " fires prev fired " << m_firedPerEfector.value(participantId); m_firedPerEfector.insert(participantId, m_firedPerEfector.value(participantId) + 1); DEBUG_LOG(m_logger) << " effector should " << m_effectorData.value(participantId).chargeAmount << " fires post fired " << m_firedPerEfector.value(participantId); if (m_firedPerEfector.value(participantId) >= m_effectorData.value(participantId).chargeAmount) { DEBUG_LOG(m_logger) << "Fired last effect, time to send confirmation in one sec."; QTimer::singleShot(1000, this, [this, participantId] { onSendConfirmationReportToTab(participantId); }); } DEBUG_LOG(m_logger) << " New shorts for participant " << participantId << " : " << m_firedPerEfector.value(participantId) << " from " << m_effectorData.value(participantId).chargeAmount; // now look for end if ALL are fired bool end = true; for (const auto &effector : m_effectorData) { if (effector.chargeAmount > m_firedPerEfector.value(effector.participantId)) end = false; } if (!chargeEffectIds.size()) { ERROR_LOG(m_logger) << "No ChargeEffects inside Vector, can't to anything!"; return; } QPair firstChargeEffect = QPair(chargeEffectIds.first(), m_mainHandler->chargeEffectHandler()->getChargeEffectById(chargeEffectIds.first())->getEffectStart()); QPair lastChargeEffect = firstChargeEffect; if (chargeEffectIds.size() > 1) // we need to find the last and first one { for (const auto id : qAsConst(chargeEffectIds)) { const auto &chargeEffect = m_mainHandler->chargeEffectHandler()->getChargeEffectById(id); if (chargeEffect->getEffectStart() > lastChargeEffect.second) lastChargeEffect = QPair(id, chargeEffect->getEffectStart()); if (chargeEffect->getEffectStart() < firstChargeEffect.second) firstChargeEffect = QPair(id, chargeEffect->getEffectStart()); } } if (end) { m_data.remainExplosionNumber = 0; onPublishData(); m_lookoutChargeEffectEnd = lastChargeEffect.first; DEBUG_LOG(m_logger) << "Set Looking for End to " << m_lookoutChargeEffectEnd; } else { m_data.remainExplosionNumber = std::max(0, m_data.remainExplosionNumber - 1); onPublishData(); } // Now if we are still in esPrepared we try to switch to fired DEBUG_LOG(m_logger) << "Try set start from chargeEffect " << m_lookoutChargeEffectStart << " " << m_data.effectStateId; if (m_data.effectStateId != AreaEffectSimulation::AesEffectState::EsPrepared && m_lookoutChargeEffectStart != 0) return; if ((m_data.effectStateId == AreaEffectSimulation::AesEffectState::EsPrepared || m_data.effectStateId == AreaEffectSimulation::AesEffectState::EsFired) && m_lookoutChargeEffectStart == 0) { if (!m_firingTimeReached) { m_firingTimeReached = true; } if (m_data.effectStateId == AreaEffectSimulation::AesEffectState::EsPrepared && !trySwitchToFired()) { WARNING_LOG(m_logger) << "Could not switch to Fired for aesid " << m_data.id; return; } // first fired ChargeEffect m_lookoutChargeEffectStart = firstChargeEffect.first; DEBUG_LOG(m_logger) << "Set Looking for Start to " << m_lookoutChargeEffectStart; // update Effect Start to REAL flight end const auto chargeEffect = m_mainHandler->chargeEffectHandler()->getChargeEffectById(m_lookoutChargeEffectStart); if (chargeEffect) { DEBUG_LOG(m_logger) << "Use Effect Start from ChargeEffect " << chargeEffect->id() << " effectstarte: " << chargeEffect->getEffectStart().toString(); m_data.effectStart = chargeEffect->getEffectStart(); } else // Fallback { DEBUG_LOG(m_logger) << "Use Effect Start fallback" << partitionTime().addMSecs(m_effectorData.value(participantId).flightTime).toString(); m_data.effectStart = partitionTime().addMSecs(m_effectorData.value(participantId).flightTime); } // Fnally publish data with ne effect start onPublishData(); } } double ObjectEffectCluster::getElevationFor(const QPointF &originGeoRadiant, const QString &context) const { static LtsElevation::LtsElevationManager *eleMgr = nullptr; if (!eleMgr) eleMgr = getTrajectoryInstance(m_mainHandler)->getElevationManager(); double zValue = s_defaultZ_Value; // default Elevation for GUEZ / Altmark. Hopefully fine for other areas.. if (eleMgr) zValue = eleMgr->elevation(originGeoRadiant.x(), originGeoRadiant.y()); // double lon_rad, double lat_rad else { WARNING_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " no ElevationManager, assuming '100', context='" << context << "'" << " zValue= " << zValue; } if (zValue < -300) { WARNING_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " elevation for originGeoRadiant '" << originGeoRadiant << "' is really small: " << zValue << ", assuming '100', context='" << context << "'"; zValue = s_defaultZ_Value; } return zValue; } bool ObjectEffectCluster::generateTrajectory(int explosionNumber, bool startDetonationLocationRequest, QString *reasonOut) { // generate Trajectory: DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " START generate Trajectory: explosionNumber = " << explosionNumber << ", startDetonationLocationRequest=" << startDetonationLocationRequest; // Q_ASSERT(isVulcano() == false); const int zoneOfImpactCoord = LtsCoordTrans::UtmCoordinate::fromGeographic(m_data.impactPosition).zone; QPointF endPosition_geo2D; const int explosionIndex = explosionNumber - 1; bool bIsGunToTargetPreview = false; if (explosionNumber <= 0) { bIsGunToTargetPreview = true; // center of target: endPosition_geo2D = AreaEffectSimulation::Polygons::bottomLeft2CenterPoint(m_data.impactPosition, m_data.impactWidth, m_data.impactLength, m_data.rotationAngle); DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " *** generateTrajectory to: center" << ", endPosition_geo2D = " << endPosition_geo2D; } else { // take impactPosition from form: const std::vector &formRef = m_aiv2.form(); Q_ASSERT(formRef.size() > 0); Q_ASSERT(explosionIndex >= 0); const Core::GeoPosition &impactPosition_geo3D = formRef[explosionIndex]; endPosition_geo2D = QPointF(impactPosition_geo3D.lon(), impactPosition_geo3D.lat()); DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " *** generateTrajectory to: explosionIndex=" << explosionIndex << ", endPosition_geo2D = " << endPosition_geo2D; } // generate 'ideal' trajectory geometry, will be adjusted via reply and adjustTrajectoryToDetonationLocation // save to m_trajectoryCont: TracjectoryData &tdRef = m_trajectoryCont[explosionIndex]; tdRef.index = explosionIndex; // reduce input values // in: a) EffectCluster.peakHeight // b) EffectCluster.flightDuration[msec] and LowOrHigh // reduce to angle: to support generateTrajectoryBiedziak const LtsCoordTrans::UtmCoordinate &endPosUtm = LtsCoordTrans::UtmCoordinate::fromGeographic(endPosition_geo2D, zoneOfImpactCoord); // add z-Value: const double zValue = getElevationFor(m_data.impactPosition, QString(QStringLiteral("impactPosition_explosionIndex_%1")).arg(explosionIndex)); const LtsUtils::QPoint3D endPosUtmPoint3D = LtsUtils::QPoint3D(endPosUtm.x, endPosUtm.y, zValue); double betaRadiant = 0; // elevation_angle_degree double vOut = 0; bool success = true; QString reason, reasonTraj; // firingOrigin.... should have an absolute zValue: // which cannon? LtsUtils::QPoint3D firingOriginGeoPoint3D; LtsUtils::QPoint3D firingOriginUtmPoint3D; if (bIsGunToTargetPreview) { firingOriginGeoPoint3D = m_data.firingCenterPosition; firingOriginUtmPoint3D = m_temporaryData.m_firingUnitOriginUtmPoint3D; } else { firingOriginGeoPoint3D = getFiringCannonGeoPosition(explosionIndex); firingOriginUtmPoint3D = getFiringCannonUtmPosition(explosionIndex, zoneOfImpactCoord); } // Which data is given by user? UI::peakHeight is relative to firingPosition.z() if (m_data.peakHeight > 0) { tdRef.peakHeightAbs = m_temporaryData.m_firingUnitOriginUtmPoint3D.z() + m_data.peakHeight; // checked: GUEZE-2310 // For flightDurationSec the center should be used. (FlightDuration should be identical for all trajectories tdRef.flightDurationMSec = TrajectoryHelper::flightDurationSec(m_temporaryData.m_firingUnitOriginUtmPoint3D, endPosUtmPoint3D, tdRef.peakHeightAbs, &betaRadiant, &vOut, &reasonTraj) * 1000; if (tdRef.flightDurationMSec <= 0 || !reasonTraj.isEmpty()) { ERROR_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << ", got flightDurationSecError: " << ", explosionIndex=" << explosionIndex << ", m_temporaryData.m_firingUnitOriginUtmPoint3D = " << m_temporaryData.m_firingUnitOriginUtmPoint3D << ", m_data.peakHeight (relToFiring) " << m_data.peakHeight << ", tdRef.peakHeightAbs " << tdRef.peakHeightAbs << ", endPosUtmPoint3D = " << endPosUtmPoint3D; success = false; reason = QString(QStringLiteral("(flightDurationSecError (%1;%2))")).arg(tdRef.flightDurationMSec).arg(reasonTraj); } } else if (m_data.flightDuration > 0) { tdRef.flightDurationMSec = m_data.flightDuration; DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << ", isAirSupportSource = " << m_isAirSupportSource; if (m_isAirSupportSource) { DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << ", endPosUtmPoint3D = " << endPosUtmPoint3D; // NASAT: firingCenterPosition is position of AirCraft, means apogaeum: const LtsCoordTrans::UtmCoordinate &firingOriginUtm = LtsCoordTrans::UtmCoordinate::fromGeographic(m_data.firingCenterPosition.toPointF(), zoneOfImpactCoord); DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << ", peak==NASAT = " << firingOriginUtm; const LtsUtils::QPoint3D apogaeum(firingOriginUtm.x, firingOriginUtm.y, m_data.firingCenterPosition.z()); const LtsUtils::QPoint3D endPoint3D = endPosUtmPoint3D; const LtsUtils::QPoint3D realStartPointUTM = TrajectoryHelper::realStartPoint(apogaeum, endPoint3D); // and now let that be used for Trajectory...: { LtsCoordTrans::UtmCoordinate temp(zoneOfImpactCoord, realStartPointUTM.x(), realStartPointUTM.y()); QPointF tempGeo = temp.geographic(); firingOriginUtmPoint3D = realStartPointUTM; firingOriginGeoPoint3D = LtsUtils::QPoint3D(tempGeo.x(), tempGeo.y(), realStartPointUTM.z()); } DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << ", XXXXX endPoint3D = " << endPoint3D << ", apogaeum = " << apogaeum << ", realStartPointUTM = " << realStartPointUTM; } tdRef.peakHeightAbs = TrajectoryHelper::peakHeightAbs(firingOriginUtmPoint3D, endPosUtmPoint3D, m_data.flightDuration / 1000.0, /*trajectoryMode,*/ &betaRadiant, &vOut, &reasonTraj); DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << ", XXXXX tdRef.peakHeightAbs = " << tdRef.peakHeightAbs; if ((tdRef.peakHeightAbs < -1000) || (tdRef.peakHeightAbs > 400000) || !reasonTraj.isEmpty()) // Beginn der Atmosphaere: 400-700km { success = false; reason = QString(QStringLiteral("(peakHeightAbs outOfRange (%1))")).arg(reasonTraj); ERROR_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << ", got peakHeight with error: " << ", reason ='" << reason << "'" << ", explosionIndex=" << explosionIndex << ", m_data.peakHeight (relToFiring) " << m_data.peakHeight << ", m_firingUnitOriginUtmPoint3D = " << m_temporaryData.m_firingUnitOriginUtmPoint3D << ", endPosUtmPoint3D = " << endPosUtmPoint3D; } } // if (m_data.peakHeight > 0) if (bIsGunToTargetPreview) { m_temporaryData.calculatedFiringSpeed = vOut; m_temporaryData.calculatedFlightDurationMsec = tdRef.flightDurationMSec; m_temporaryData.calculatedPeakHeightAbs = tdRef.peakHeightAbs; { // calculate mhpRotation using UTM and identical zone! m_temporaryData.calculatedMhpRotationNorthDeg = getAngleDegreeNorth(m_temporaryData.m_firingUnitOriginUtmPoint3D.toPointF(), endPosUtmPoint3D.toPointF()); DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << ", angleDegreeNorth = " << m_temporaryData.calculatedMhpRotationNorthDeg; } } if (success) { const double elevation_angle_degree = betaRadiant * LtsCoordTrans::rad2deg(); if ((elevation_angle_degree < 0) || (elevation_angle_degree > 90)) { success = false; reason += QString(QStringLiteral("(elevation_angle_degree outOfRange (%1))")).arg(elevation_angle_degree); } if (success) { m_temporaryData.calculatedFiringAngle = elevation_angle_degree; DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " XXXXX explosionIndex=" << explosionIndex << ", firingOriginGeoPoint3D " << firingOriginGeoPoint3D << ", firingOriginUtmPoint3D " << firingOriginUtmPoint3D << ", m_data.peakHeight (relToFiring) " << m_data.peakHeight << ", tdRef.peakHeightAbs " << tdRef.peakHeightAbs << ", m_data.flightDuration=" << m_data.flightDuration << ", endPosUtmPoint3D = " << endPosUtmPoint3D << " -> tdRef.flightDurationMSec = " << tdRef.flightDurationMSec << ", elevation_angle_degree = " << elevation_angle_degree << ", vOut = " << vOut; // create data in place (tdRef.trajectoryGeometry) static TrajectoryGenerator *trajectoryGenerator = getTrajectoryInstance(m_mainHandler); success = trajectoryGenerator->generateTrajectoryBiedziak(firingOriginGeoPoint3D, LtsUtils::QPoint3D(endPosition_geo2D.x(), endPosition_geo2D.y(), zValue), elevation_angle_degree, tdRef.trajectoryGeometry); // for NASAT wa can remove first half: if (m_isAirSupportSource) { // Note: tdRef.trajectoryGeometry is not time-aware // not really nice but: since pop_front is not available create a temorary partial copy; // avoid start in den back of the AirCraft std::vector trajectoryGeometrySecondHalf(tdRef.trajectoryGeometry.begin() + (tdRef.trajectoryGeometry.size() / 2.0), tdRef.trajectoryGeometry.end()); DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " XXXXX explosionIndex=" << explosionIndex << " tdRef.trajectoryGeometry.size" << tdRef.trajectoryGeometry.size() << " tdRef.trajectoryGeometrySecondHalf.size" << trajectoryGeometrySecondHalf.size(); tdRef.trajectoryGeometry = trajectoryGeometrySecondHalf; } if (!success) { ERROR_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " ERROR *** trajectoryGenerator->generateTrajectory failed, explosionIndex=" << explosionIndex; reason += QStringLiteral("(generateTrajectory failed)"); } Core::GeoPosition first; if (!tdRef.trajectoryGeometry.empty()) first = tdRef.trajectoryGeometry.front(); DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " *** generateTrajectory, explosionIndex=" << explosionIndex << " DONE" << ", trajectory.size = " << tdRef.trajectoryGeometry.size() << ", first = " << first; if (startDetonationLocationRequest) { // Schnittpunktberechnung nur bei bei zentralen Wirkfeldern if (m_data.isCentral) { // start DetonationLocation request const DetonationLocationRequest &request = detonationLocationRequest(explosionIndex); m_mainHandler->dds()->cesFtc()->detonationLocationRequest(request, this); } else { DetonationLocationReply reply; reply.id(m_data.id); reply.index(explosionIndex); // reply.position(); // leave on default :-) onDetonationLocationReply(reply); } // reply is processed at onDetonationLocationReply } } // if (success) } // if (success) if (reasonOut) *reasonOut = reason; DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " END generate Trajectory: explosionNumber = " << explosionNumber << ", startDetonationLocationRequest=" << startDetonationLocationRequest; return success; } bool ObjectEffectCluster::generateTrajectoryVulcanoSpecific(int explosionNumber, const TrajectoryGeneratorVulcano::Options options, QString *reasonOut) { // const int zoneOfImpactCoord = LtsCoordTrans::UtmCoordinate::fromGeographic(m_data.impactPosition).zone; const int explosionIndex = explosionNumber - 1; const bool bIsGunToTargetPreview = true; // center of target: const QPointF &endPosition_geo2D = AreaEffectSimulation::Polygons::bottomLeft2CenterPoint(m_data.impactPosition, m_data.impactWidth, m_data.impactLength, m_data.rotationAngle); DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " *** generateTrajectory to: center" << ", endPosition_geo2D = " << endPosition_geo2D << ", explosionNumber=" << explosionNumber; // save to m_trajectoryCont: TracjectoryData &tdRef = m_trajectoryCont[explosionIndex]; tdRef.index = explosionIndex; // which cannon? LtsUtils::QPoint3D firingOriginGeoPoint3D; // LtsUtils::QPoint3D firingOriginUtmPoint3D; if (bIsGunToTargetPreview) { firingOriginGeoPoint3D = m_data.firingCenterPosition; // firingOriginUtmPoint3D = m_temporaryData.m_firingUnitOriginUtmPoint3D; } else { firingOriginGeoPoint3D = getFiringCannonGeoPosition(explosionIndex); // firingOriginUtmPoint3D = getFiringCannonUtmPosition(explosionIndex, zoneOfImpactCoord); } bool success = false; QString localReasonOut; // Which data is given by user? UI::peakHeight is relative to firingPosition.z() if (m_data.peakHeight > 0) { const double v0 = TrajectoryGeneratorVulcano::defaultv0(); const double peakHeightRelToOrigin = m_data.peakHeight; const double betaRad = XY::calculateBetaRad(v0, peakHeightRelToOrigin, &localReasonOut); DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " START" << " id/name= '" << m_data.name << "'/" << m_data.id << " IS_VULCANO = " << " betaDeg " << betaRad * LtsCoordTrans::rad2deg(); if (!localReasonOut.isEmpty()) { ERROR_LOG(m_logger) << "generateTrajectoryVulcano failed, localReasonOut = " << localReasonOut; if (reasonOut) *reasonOut = localReasonOut; return false; } const double alphaRad = m_data.dropAngle * LtsCoordTrans::deg2rad(); const double tapTime = TrajectoryGeneratorVulcano::defaultTapTimeSec(); m_data.launchAngleDeg = betaRad * LtsCoordTrans::rad2deg(); // const TrajectoryGeneratorVulcano::Options opts = // TrajectoryGeneratorVulcano::OptRasterApoTap | // TrajectoryGeneratorVulcano::OptIncludeIdealHit | // TrajectoryGeneratorVulcano::OptRasterTapHit; const QPoint3D &originatorPosition_geo = firingOriginGeoPoint3D; // add z-Value: const double zValue = getElevationFor(m_data.impactPosition, QString(QStringLiteral("impactPosition_explosionIndex_%1")).arg(explosionIndex)); const QPoint3D idealHit_geo(endPosition_geo2D.x(), endPosition_geo2D.y(), zValue); static TrajectoryGenerator *trajectoryGenerator = getTrajectoryInstance(m_mainHandler); Core::GeoPosition idealTapAbsoluteGeoOut; success = trajectoryGenerator->generateTrajectoryVulcano(originatorPosition_geo, idealHit_geo, v0, betaRad, alphaRad, tapTime, options, tdRef.trajectoryTimeAware.geometry(), idealTapAbsoluteGeoOut, &localReasonOut); if (!localReasonOut.isEmpty()) { ERROR_LOG(m_logger) << "generateTrajectoryVulcano failed, localReasonOut = " << localReasonOut; if (reasonOut) *reasonOut = localReasonOut; return false; } if (!success) { ERROR_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " START" << " id/name= '" << m_data.name << "'/" << m_data.id << " trajectoryGenerator->generateTrajectoryVulcano failed"; if (reasonOut) *reasonOut = localReasonOut; return false; } DEBUG_LOG(m_logger) << " cloudBase modification = " << m_data.cloudBase; // honor cloudBase if valid: if (m_data.cloudBase > std::numeric_limits::min()) { // get access to point tapT: std::vector &geometryRef = tdRef.trajectoryTimeAware.geometry(); std::vector::reverse_iterator rit = geometryRef.rbegin(); const Core::GeoPositionRelativeTime impact = *rit; // one step to point to tapT ++rit; const Core::GeoPositionRelativeTime tapT = *rit; const bool bTapT_strichRequested = (m_data.cloudBase < tapT.z()) && (impact.z() < m_data.cloudBase); DEBUG_LOG(m_logger) << " XXX cloudBase modification: " << ", tapT = " << tapT << ", m_data.cloudBase = " << m_data.cloudBase << ", impact = " << impact << ", bTapT_strichRequested = " << bTapT_strichRequested; if (bTapT_strichRequested) { const LtsCoordTrans::UtmCoordinate impactUtm = LtsCoordTrans::UtmCoordinate::fromGeographic(impact.lon(), impact.lat()); const LtsCoordTrans::UtmCoordinate tapTUtm = LtsCoordTrans::UtmCoordinate::fromGeographic(tapT.lon(), tapT.lat(), impactUtm.zone); const QVector3D impactVec3D(impactUtm.x, impactUtm.y, impact.z()); const QVector3D tapTVec3D(tapTUtm.x, tapTUtm.y, tapT.z()); /* x | Tap (aka TapT) // \ | // \ | // \ | // \ | // -------x | CloudBase (Tap_strich) // \ | // \ | // ----------x | Impact // // ----------------| Surface */ // Geometry // aus Sicht von Impact QVector3D offset = tapTVec3D - impactVec3D; const double offset_z = offset.z(); const double scaleFactor = (static_cast(m_data.cloudBase) - impactVec3D.z()) / offset_z; const QVector3D scaledOffset = offset * scaleFactor; const QVector3D tapT_strich = impactVec3D + scaledOffset; // DEBUG_LOG(m_logger) << " offset_z " << offset_z; // DEBUG_LOG(m_logger) << " scaleFactor " << scaleFactor; // DEBUG_LOG(m_logger) << " scaledOffset " << scaledOffset; // DEBUG_LOG(m_logger) << " tapTVec3D " << tapTVec3D; // DEBUG_LOG(m_logger) << " offset " << offset; // DEBUG_LOG(m_logger) << " tapT_strich " << tapT_strich; // DEBUG_LOG(m_logger) << " impactVec3D " << impactVec3D; const LtsCoordTrans::UtmCoordinate tapT_strichUtm(impactUtm.zone, tapT_strich.x(), tapT_strich.y()); const QPointF &tapT_strichGeo = tapT_strichUtm.geographic(); // Time double timeOffset = impact.relativeTime() - tapT.relativeTime(); timeOffset *= scaleFactor; Core::GeoPositionRelativeTime tapT_strichRelTime; tapT_strichRelTime.lon() = tapT_strichGeo.x(); tapT_strichRelTime.lat() = tapT_strichGeo.y(); tapT_strichRelTime.z() = tapT_strich.z(); tapT_strichRelTime.relativeTime(impact.relativeTime() - timeOffset); DEBUG_LOG(m_logger) << " tapT_strichRelTime " << tapT_strichRelTime; // (.. n-4, n-3, n-2, n-1, n) // ^ // Inserts value before pos: auto it = geometryRef.end(); geometryRef.insert(--it, tapT_strichRelTime); } } // "convert" from time-aware curve 'back' to time-unaware trajectoryGeometry tdRef.trajectoryGeometry.clear(); for (const Core::GeoPositionRelativeTime &geoPosRelTime : qAsConst(tdRef.trajectoryTimeAware.geometry())) tdRef.trajectoryGeometry.emplace_back(geoPosRelTime); // implicit // downcast tdRef.peakHeightAbs = peakHeightRelToOrigin + originatorPosition_geo.z(); } else { // For vulcano, peakHeight is required, not optional localReasonOut = QString(QStringLiteral("peakHeight is invalid (%1)")).arg(m_data.peakHeight); DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " START" << " id/name= '" << m_data.name << "'/" << m_data.id << " no peakHeight"; } if (reasonOut) *reasonOut = localReasonOut; DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " END generate Trajectory: explosionNumber = " << explosionNumber; return success; } bool ObjectEffectCluster::getFiringUnitOrigin(const int firingParticipantId, LtsUtils::QPoint3D &firingOriginOut, QString *reasonOut) { // firingParticipantId is Feuereinheit/GeschuetzStellung TRACE_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " START, firingParticipantId = " << firingParticipantId; QString reason; if (firingParticipantId <= 0) { ERROR_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << ", participantId invalid: " << firingParticipantId; reason = QString(QStringLiteral("firingParticipantId is invalid (%1)")).arg(firingParticipantId); if (reasonOut) *reasonOut = reason; return false; } std::shared_ptr ppmDtc = m_mainHandler->dds()->ppmDtc(); if (!ppmDtc) { ERROR_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << ", ERROR, no ppmDtc"; reason = QString(QStringLiteral("firingParticipantId not found (%1)")).arg(firingParticipantId); if (reasonOut) *reasonOut = reason; return false; } const bool isLeaf = ppmDtc->participant(firingParticipantId).inferiorIds().empty(); // isLeaf means single (Participant and not a Unit) if (isLeaf) { const ParticipantManagement::ParticipantPosition &partPos = ppmDtc->participantPosition(firingParticipantId); if (partPos.participantId() != firingParticipantId) return false; // relative zValue -> absolute zValue: // Keep NASAT in mind sending 'height' via NasatPosition (normally, 'height' is relative to ground) const double zValue = getElevationFor({partPos.pos().lon(), partPos.pos().lat()}, QString(QStringLiteral("participantPosition_partId=%1")).arg(firingParticipantId)); firingOriginOut = LtsUtils::QPoint3D(partPos.pos().lon(), partPos.pos().lat(), partPos.pos().z() + zValue); } else { // See also [GUEZE-2309] // Using the ppmDtc->unitPosition has some drawbacks: // The PPM::UnitPositionCalculator ignore outdated positions // So in case of Replay/Snapshot/Restart of SV-SW the resulting position may differ // So calculate on our own: // const ParticipantManagement::UnitPosition &unitPos = ppmDtc->unitPosition(firingParticipantId); // if (unitPos.participantId() != firingParticipantId) // return false; firingOriginOut = Polygons::calculateFiringUnitCenterNonRecursive(m_mainHandler->dds()->ppmDtc(), firingParticipantId, m_temporaryData.singleFiringPositions, m_logger); // set z-Value: int idx = 0; for (Core::GeoPosition &geoPosRef : m_temporaryData.singleFiringPositions) { const double zValue = getElevationFor({geoPosRef.lon(), geoPosRef.lat()}, QString(QStringLiteral("firingCannonPosition_index_%1")).arg(idx++)); geoPosRef.z(zValue); } if (m_logger.isDebugEnabled()) for (const Core::GeoPosition geoPosRef : m_temporaryData.singleFiringPositions) DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " singleFiringPositions = " << geoPosRef; } // and some checks: if (std::isnan(firingOriginOut.x()) || std::isnan(firingOriginOut.y())) { ERROR_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " invalid position for firingParticipantId = " << firingParticipantId << ", firingOrigin is NaN"; reason = QString(QStringLiteral("firingParticipantId '%1' has invalid position (NaN)")).arg(firingParticipantId); if (reasonOut) *reasonOut = reason; return false; } if (qFuzzyIsNull(firingOriginOut.x()) || qFuzzyIsNull(firingOriginOut.y())) { ERROR_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " invalid position for firingParticipantId = " << firingParticipantId << ", firingOrigin is 0"; reason = QString(QStringLiteral("firingParticipantId '%1' has invalid position (0,0)")).arg(firingParticipantId); if (reasonOut) *reasonOut = reason; return false; } // const double zValue = getElevationFor(firingOriginOut.toPointF(), QString(QStringLiteral("firingParticipantId%1")).arg(firingParticipantId)); // firingOriginOut.setZ(zValue); // check provided zValue; add elevation if neccessary: // (e.g. NASAT aircrafts are already having a valid zValue, do not overwrite) double zValue = firingOriginOut.z(); if (std::isnan(zValue) || qFuzzyIsNull(zValue)) { zValue = getElevationFor(firingOriginOut.toPointF(), QString(QStringLiteral("firingParticipantId%1")).arg(firingParticipantId)); firingOriginOut.setZ(zValue); } m_temporaryData.m_firingUnitOriginUtm = LtsCoordTrans::UtmCoordinate::fromGeographic(firingOriginOut.toPointF()); m_temporaryData.m_firingUnitOriginUtmPoint3D = LtsUtils::QPoint3D(m_temporaryData.m_firingUnitOriginUtm.x, m_temporaryData.m_firingUnitOriginUtm.y, zValue); DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " END, firingParticipantId = " << firingParticipantId << ", firingOriginOut = " << firingOriginOut << ", m_firingUnitOriginUtm = " << m_temporaryData.m_firingUnitOriginUtm.mgrs() << ", m_firingUnitOriginUtmPoint3D = " << m_temporaryData.m_firingUnitOriginUtmPoint3D; return true; } bool ObjectEffectCluster::firingOriginIsValid(const LtsUtils::QPoint3D &firingOrigin, QString *reasonOut) const { TRACE_LOG(m_logger) << LOGFUNCTION << " START, originatorOrigin=" << firingOrigin; // check if valid // Maybe in future we will check also the distance to the target if (std::isnan(firingOrigin.x()) || std::isnan(firingOrigin.y()) || std::isnan(firingOrigin.z())) { if (reasonOut) *reasonOut = QStringLiteral("originatorOrigin isNaN"); TRACE_LOG(m_logger) << LOGFUNCTION << " END, originatorOrigin isNaN, " << firingOrigin; return false; } if (firingOrigin.isNull()) { if (reasonOut) *reasonOut = QStringLiteral("originatorOrigin isNull"); TRACE_LOG(m_logger) << LOGFUNCTION << " END, originatorOrigin isNull, " << firingOrigin; return false; } TRACE_LOG(m_logger) << LOGFUNCTION << " END, originatorOrigin=" << firingOrigin; return true; } LtsUtils::QPoint3D ObjectEffectCluster::getFiringCannonGeoPosition(int explosionIndex) const { Core::GeoPosition geoPos = m_temporaryData.getFiringCannonGeoPosition(explosionIndex); // in case of missing coordinates for single cannons (or ModuleTest): if (std::isnan(geoPos.lon()) || std::isnan(geoPos.lat()) || std::isnan(geoPos.z())) return m_data.firingCenterPosition; return LtsUtils::QPoint3D(geoPos.lon(), geoPos.lat(), geoPos.z()); } LtsUtils::QPoint3D ObjectEffectCluster::getFiringCannonUtmPosition(int explosionIndex, int zoneToUse) const { const LtsUtils::QPoint3D &geoPoint3D = ObjectEffectCluster::getFiringCannonGeoPosition(explosionIndex); const LtsCoordTrans::UtmCoordinate &temp = LtsCoordTrans::UtmCoordinate::fromGeographic(geoPoint3D.toPointF(), zoneToUse); return LtsUtils::QPoint3D(temp.x, temp.y, geoPoint3D.z()); } void ObjectEffectCluster::sendCalculateTrajectoryAdlerRequest(const QVector &effectorData) { if (m_data.effectStateId == AesEffectState::EsExpired) return; QString reason; for (const auto &data : qAsConst(effectorData)) { DEBUG_LOG(m_logger) << "TRY to send Calcualte TrajecotryAdlerReuqest for participantId=" << data.participantId; const ParticipantManagement::ParticipantPosition &partPos = m_mainHandler->dds()->ppmDtc()->participantPosition(data.participantId); LtsUtils::QPoint3D geoPosOut; if (!getFiringUnitOrigin(data.participantId, geoPosOut, &reason)) { ERROR_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " getFiringOrigin failed for id " << data.id << ", reason: '" << reason << "'"; return; } const Core::GeoPosition positionLaunch(geoPosOut.x(), geoPosOut.y(), geoPosOut.z()); const Core::GeoPosition positionTarget(data.mhpPosition.x(), data.mhpPosition.y(), data.mhpPosition.z()); DEBUG_LOG(m_logger) << "SAA is connected partyId: " << m_data.partyId << ", participantId: " << data.participantId << " --------------------- "; DEBUG_LOG(m_logger) << "positionLaunch = " << positionLaunch; DEBUG_LOG(m_logger) << "positionTarget = " << positionTarget; // parse Properties const QJsonObject &probObject = QJsonDocument::fromJson(QByteArray::fromStdString(m_data.properties.toStdString())).object(); int loadTypeId = m_mainHandler->dds()->pdmDtc()->getLoadTypeIdFromAdlerLoad(QStringLiteral("B L8A1")); if (probObject.contains(AdlerJson::JsonKeys::LADUNG())) loadTypeId = probObject[AdlerJson::JsonKeys::LADUNG()].toInt(); ProjectDataManagement::L43_AngleGroup angleGroup = ProjectDataManagement::L43_AngleGroup::Lower; if (probObject.contains(AdlerJson::JsonKeys::WINKELGRUPPE())) angleGroup = static_cast(probObject[AdlerJson::JsonKeys::WINKELGRUPPE()].toInt()); // Send request TrainingAreaBase::AdlerCalculateTrajectoryRequest request; const int partyId = m_mainHandler->dds()->ppmDtc()->participant(data.participantId).partyId(); request.partyId(partyId); request.positionLaunch(positionLaunch); request.aesItemId(m_data.id); request.positionTarget(positionTarget); request.properties(m_data.properties.toStdString()); request.trajectory().chargeTypeId() = data.chargeTypeId; request.trajectory().igniterFunctionId() = data.igniterFunctionId; request.trajectory().igniterTypeId() = data.igniterTypeId; request.trajectory().loadTypeId() = loadTypeId; const ProjectDataManagement::GearClass &gc = m_mainHandler->dds()->pdmDtc()->gearClassFromChargeTypeId(m_data.chargeTypeId); switch (gc.underlying()) { case ProjectDataManagement::GearClass::Artillery: request.weaponSystemType(TrainingAreaBase::WeaponSystemType::PZH); break; case ProjectDataManagement::GearClass::Mortar: request.weaponSystemType(TrainingAreaBase::WeaponSystemType::MRS); break; case ProjectDataManagement::GearClass::Rocket: request.weaponSystemType(TrainingAreaBase::WeaponSystemType::MARS); break; default: request.weaponSystemType(TrainingAreaBase::WeaponSystemType::PZH); break; } request.trajectory().loadNumber() = probObject[AdlerJson::JsonKeys::LADUNGSNUMMER()].toInt(); request.angleGroup(angleGroup); INFO_LOG(m_logger) << "call TAB::calculateTrajectoryAdler" << ", participantId: " << data.participantId << ", partyId: " << partyId << ", positionLaunch: " << positionLaunch << ", positionTarget: " << positionTarget << ", m_data.impactCount: " << m_data.impactCount << ", m_data.id aesItemId: " << m_data.id; // for ( int i = 0; i < m_data.impactCount; i++) { DEBUG_LOG(m_logger) << "Trajectory Index:" << data.participantId; request.explosionIndex(data.participantId); if (m_mainHandler->dds()->tabFtc() && m_mainHandler->dds()->tabFtc()->isAdlerCalculateTrajectoryEnabled()) { m_mainHandler->dds()->tabFtc()->adlerCalculateTrajectoryRequest(request, this); } else WARNING_LOG(m_logger) << "Could not ask ADLER for Trajectory"; } } } void ObjectEffectCluster::onAdlerCalculateTrajectoryReply(const Core::FunctionReplyObject &replyObject) { DEBUG_LOG(m_logger) << "onSendCalculateTrajectoryAdlerReply " << ", replyObject.id() :" << replyObject.id() << ", m_explosionIndex :" << m_explosionIndex << ", replyObject.error :" << replyObject.error() << ", replyObject.str() :" << replyObject.str() << ", replyObject.translated() :" << replyObject.translated(); if (replyObject.error() > 0) { DEBUG_LOG(m_logger) << "onSendCalculateTrajectoryAdlerReply replyObject.error() :" << replyObject.error() << ", sende EffectClusterModifyReply"; m_temporaryData.annotationString = replyObject.translated().toStdString(); Core::FunctionReplyObject ret; m_mainHandler->dds()->fts()->effectClusterModifyReply(m_cookie, ret); onPublishData(); return; } if (m_effectorData.contains(replyObject.id())) { // m_explosionIndex++; m_temporaryData.annotationString.clear(); DEBUG_LOG(m_logger) << "onSendCalculateTrajectoryAdlerReply m_explosionIndex=" << m_explosionIndex; if (m_explosionIndex >= m_data.impactCount) { DEBUG_LOG(m_logger) << " letztes onSendCalculateTrajectoryAdlerReply" << ", sende EffectClusterModifyReply ohne Fehler"; Core::FunctionReplyObject ret; m_mainHandler->dds()->fts()->effectClusterModifyReply(m_cookie, ret); return; } DEBUG_LOG(m_logger) << "onSendCalculateTrajectoryAdlerReply sendCalculateTrajectoryAdlerRequest"; // sendCalculateTrajectoryAdlerRequest(); } DEBUG_LOG(m_logger) << "onCalculateTrajectoryAdlerReply for id " << replyObject.id() << " success= " << replyObject.error(); } void ObjectEffectCluster::onSendExFireReadinessReply(const Core::FunctionReplyObject &reply) { DEBUG_LOG(m_logger) << "onSendExFireReadinessReply for id " << reply.id() << " success= " << reply.error(); } void ObjectEffectCluster::onSendExFiredReportReply(const Core::FunctionReplyObject &reply) { DEBUG_LOG(m_logger) << "onSendExFiredReportReply for id " << reply.id() << " success= " << reply.error(); } void ObjectEffectCluster::onSendExConfirmationReportReply(const Core::FunctionReplyObject &reply) { DEBUG_LOG(m_logger) << "onSendExConfirmationReportReply for id " << reply.id() << " success= " << reply.error(); } std::vector ObjectEffectCluster::generateGunToTargetForm(const std::vector &trajectoryRelativeToOriginator, int participantId) { bool isSaaConnected = (m_mainHandler->dds()->ppmDtc()->saaConnectedState(m_data.partyId) == ParticipantManagement::ConnectState::Connected); DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " START generateGunToTargetForm" << ", partyId: " << m_data.partyId << ", participantId: " << m_data.participantId << ", isSaaConnected:" << isSaaConnected << "---"; // origin has to be setup (via setupParticipantOrigin) std::vector trajOut; QString reason; /* if (isSaaConnected && input.empty()) // generate via adler sendCalculateTrajectoryAdlerRequest(); */ if (isVulcano() && trajectoryRelativeToOriginator.empty()) { TrajectoryGeneratorVulcano::Options opts = // TrajectoryGeneratorVulcano::OptRasterApoTap | TrajectoryGeneratorVulcano::OptIncludeIdealHit // | TrajectoryGeneratorVulcano::OptRasterTapHit ; generateTrajectoryVulcanoSpecific(0, opts, &reason); // explosionNumber = 0 -> explosionIndex = -1, means to center of area } else if (trajectoryRelativeToOriginator.empty()) { generateTrajectory(0, false, &reason); // explosionNumber = 0 -> explosionIndex = -1, means to center of area } else { // we already have one just add the stuff Core::GeoPosition pos; m_mainHandler->dds()->ppmDtc()->waitForHistoricalDataParticipantPosition(2000); const auto &participantPos = m_mainHandler->dds()->ppmDtc()->participantPosition(participantId); INFO_LOG(m_logger) << "got pos for " << participantId << " " << participantPos; // Append the start pos???? // trajOut.push_back(participantPos.pos()); for (const auto adlerPos : trajectoryRelativeToOriginator) { auto transPos = LtsCoordTrans::UtmCoordinate::fromGeographic(participantPos.pos().lon(), participantPos.pos().lat()); transPos.x += adlerPos.offsetX(); transPos.y += adlerPos.offsetY(); pos.lat() = transPos.geographic().y(); // participantPos.pos().lat() + (adlerPos.lat() * (M_PI / 180.0)); pos.lon() = transPos.geographic().x(); // participantPos.pos().lon() + (adlerPos.lon() * (M_PI / 180.0)); pos.z() = participantPos.pos().z() + adlerPos.offsetZ(); trajOut.push_back(pos); } } if (!reason.isEmpty()) { ERROR_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " reason: " << reason; return {}; } // explosionNumber = 0 -> explosionIndex = -1, means to center of area // const std::vector &trajOut = m_trajectoryCont[-1].trajectoryGeometry; TRACE_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " END, GunToTarget.size = " << trajOut.size(); return trajOut; } std::vector ObjectEffectCluster::generateGunToTargetForm(QString *reasonOut) { bool isSaaConnected = (m_mainHandler->dds()->ppmDtc()->saaConnectedState(m_data.partyId) == ParticipantManagement::ConnectState::Connected); DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " START generateGunToTargetForm" << ", partyId: " << m_data.partyId << ", participantId: " << m_data.participantId << ", isSaaConnected:" << isSaaConnected << "---"; // origin has to be setup (via setupParticipantOrigin) QString reason; if (isVulcano()) { TrajectoryGeneratorVulcano::Options opts = // TrajectoryGeneratorVulcano::OptRasterApoTap | TrajectoryGeneratorVulcano::OptIncludeIdealHit // | TrajectoryGeneratorVulcano::OptRasterTapHit ; generateTrajectoryVulcanoSpecific(0, opts, &reason); // explosionNumber = 0 -> explosionIndex = -1, means to center of area } else { generateTrajectory(0, false, &reason); // explosionNumber = 0 -> explosionIndex = -1, means to center of area } if (!reason.isEmpty()) { ERROR_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " reason: " << reason; if (reasonOut) *reasonOut = reason; return {}; } // explosionNumber = 0 -> explosionIndex = -1, means to center of area const std::vector &trajOut = m_trajectoryCont[-1].trajectoryGeometry; TRACE_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " END, GunToTarget.size = " << trajOut.size(); return trajOut; } std::vector ObjectEffectCluster::reduceSMArtTrajectoryToSingle( const std::vector &trajectoryRelative) { // E01562-294: Falsche Darstellung Flugkurve SMArt // At SMArt ADLER (or SAA?) generates a polyline which is "dividing": // x x // x x // x x x // x x x // | Start | T1 | T2 (Target 1 & Target 2) // Same diagram, but with order // x4 x5 // x3 x6 // x2 x7 x9 // x1 x8 x10 // The relativeTime step backward between x8 and x9. // Approach: // Reduce to curve 0 (x1...x8): // Where is a step backward in time? TrajectorySplitter> splitter(trajectoryRelative); const auto &trajectory1 = splitter.getTrajectory1(); if (!trajectory1.empty()) return trajectory1; return trajectoryRelative; } void ObjectEffectCluster::publishTrajectory(int explosionIndex) { DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " START publishTrajectory" << ", explosionIndex=" << explosionIndex; if (m_mainHandler->dds()->ppmDtc()->saaConnectedState(m_data.partyId) == ParticipantManagement::ConnectState::Connected) { DEBUG_LOG(m_logger) << "publishTrajectory isSaaConnected => keine Trajectory verschicken!"; return; } if (!m_trajectoryCont.contains(explosionIndex)) { ERROR_LOG(m_logger) << "unknown explosionIndex " << ", m_data.id = " << m_data.id << ", explosionIndex = " << explosionIndex; return; } // enable access to TracjectoryData and add time-awarenes TracjectoryData &tdRef = m_trajectoryCont[explosionIndex]; TrajectoryTimeAware &trajectoryTimeAwareRef = tdRef.trajectoryTimeAware; trajectoryTimeAwareRef.id(1000 * m_data.id + 100 + explosionIndex); trajectoryTimeAwareRef.sampleTime(partitionTimeMSec()); // trajectoryTimeAwareRef.aesItemId(m_data.id); // trajectoryTimeAwareRef.aesTypeId(AesType::AtEffectClusterSingleTrajectory); trajectoryTimeAwareRef.index(explosionIndex); // [E01562-259] use originatorId, originatorTypeId (instead of aesItemId, aesTypeId) trajectoryTimeAwareRef.originatorId(m_data.id); // m_data.id == aesItemId trajectoryTimeAwareRef.originatorTypeId(ProjectDataManagement::StateChangeOriginator::AreaEffectSimulation); if (isVulcano()) { // Time: trajectoryTimeAwareRef.trajectoryStartTime(m_data.effectStart.toMSecsSinceEpoch() - m_temporaryData.calculatedFlightDurationMsec); bool bPublished = m_mainHandler->dds()->dts()->publish(trajectoryTimeAwareRef); if (!bPublished) ERROR_LOG(m_logger) << LOGFUNCTION << " ERROR, publish failed" << m_data.id << " explosionIndex=" << explosionIndex << ", trajectoryStartTime = " << trajectoryTimeAwareRef.trajectoryStartTime() << ", geometry().size() = " << trajectoryTimeAwareRef.geometry().size(); } else { // Time: // use sample time since FiringTimer is initiates trajectory calculation trajectoryTimeAwareRef.trajectoryStartTime(trajectoryTimeAwareRef.sampleTime()); const std::vector &trajectoryGeometry = tdRef.trajectoryGeometry; Core::GeoPosition first; if (!trajectoryGeometry.empty()) first = trajectoryGeometry.front(); DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " trajectoryGeometry=" << trajectoryGeometry.size() << ", first = " << first; std::vector &geometryRef = trajectoryTimeAwareRef.geometry(); geometryRef.clear(); geometryRef.reserve(trajectoryGeometry.size()); int idx = 0; const float flightDurationMSecSeg = static_cast(tdRef.flightDurationMSec) / (trajectoryGeometry.size() - 1); // enforce float calculation #define REDUCED_DISTRIBUTION // [GUEZE-2462] Darstellung Flugparabel Steilfeuer #ifdef REDUCED_DISTRIBUTION const int trajSize = trajectoryGeometry.size(); for (const Core::GeoPosition &pos3D : qAsConst(trajectoryGeometry)) { // if (!m_isAirSupportSource) { // High resolution at beginning to enable initial visualisation of trajectory at map display if ((trajSize > 20) && (idx > 1)) { // !m_isAirSupportSource: // In der erste Haelfte der Kurve (Steigphase) nur jedes 3 item nutzen... if ((!m_isAirSupportSource) && (idx < (trajSize / 2.0)) && (idx % 3)) // modulo 3, also nur jeden dritten nutzen { ++idx; continue; } // .. ansonsten (in der zweiten Haelfte / Fallphase) nur jedes 2 item nutzen... aber die letzten 10 direkt: else if (idx < (trajSize - 10) && (idx % 2)) // modulo 2, also nur jeden zweiten nutzen { ++idx; continue; } } // if ((trajSize > 20) && (idx>1)) } // if (!m_isAirSupportSource) geometryRef.emplace_back(Core::GeoPositionRelativeTime(pos3D.lon(), pos3D.lat(), pos3D.z(), idx * flightDurationMSecSeg)); ++idx; } // for #else for (const Core::GeoPosition &pos3D : qAsConst(trajectoryGeometry)) { geometryRef.emplace_back(Core::GeoPositionRelativeTime(pos3D.lon(), pos3D.lat(), pos3D.z(), idx * flightDurationMSecSeg)); ++idx; } // for #endif DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " explosionIndex=" << explosionIndex << " has flightDurationMSecSeg=" << flightDurationMSecSeg << ", startTime = " << trajectoryTimeAwareRef.trajectoryStartTime() << ", geometry().size() = " << trajectoryTimeAwareRef.geometry().size(); bool bPublished = m_mainHandler->dds()->dts()->publish(trajectoryTimeAwareRef); if (!bPublished) ERROR_LOG(m_logger) << LOGFUNCTION << " ERROR, publish failed" << m_data.id << " explosionIndex=" << explosionIndex << " has flightDurationMSecSeg=" << flightDurationMSecSeg << ", startTime = " << trajectoryTimeAwareRef.trajectoryStartTime() << ", geometry().size() = " << trajectoryTimeAwareRef.geometry().size(); } m_publisedTrajectoriesForDebug[explosionIndex] = QString(QStringLiteral("trajectoryPublished m_data.id=%1 - explosionIndex=%2 - trajectoryStarttime=%3 - size=%4")) .arg(m_data.id) .arg(explosionIndex) .arg(trajectoryTimeAwareRef.trajectoryStartTime()) .arg(trajectoryTimeAwareRef.geometry().size()); DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " add to m_publisedTrajectoriesForDebug " << m_publisedTrajectoriesForDebug[explosionIndex]; DEBUG_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " add to m_publisedTrajectoriesForDebug.size = " << m_publisedTrajectoriesForDebug.size(); TRACE_LOG(m_logger) << LOGFUNCTION << " " << m_data.id << " XXXX 4 trajectoryTimeAware.geometry().size() = " << trajectoryTimeAwareRef.geometry().size() // << ", bPublished=" << bPublished ; } AreaEffectSimulation::EffectorData ObjectEffectCluster::fromDatabase(const Database::AesEffectorData &data) { EffectorData ret; ret.angleOfFall() = data.angleOfFall; ret.chargeAmount() = data.chargeAmount; ret.chargeStruct().chargeTypeId() = data.chargeTypeId; ret.trajectoryRelative() = TrajectoryRelative::fromString(data.trajectory); ret.flightTime() = data.flightTime; ret.chargeStruct().igniterTypeId() = data.igniterTypeId; ret.chargeStruct().igniterFunctionId() = data.igniterFunctionId; ret.interval() = data.interval; ret.participantId() = data.participantId; ret.peakHeight() = data.peakHeight; ret.mhpPosition().lat() = data.mhpPosition.y(); ret.mhpPosition().lon() = data.mhpPosition.x(); ret.mhpPosition().z() = data.mhpPosition.z(); ret.mhpRotation() = data.mhpRotation; ret.mhpScatterLenght() = data.mhpScatterLenght; ret.mhpScatterLengthDetonationHeight() = data.mhpScatterLengthDetonationHeight; ret.mhpScatterWidth() = data.mhpScatterWidth; ret.mhpScatterWidthDetonationHeight() = data.mhpScatterWidthDetonationHeight; ret.properties() = data.properties.toStdString(); return ret; } DetonationLocationRequest ObjectEffectCluster::detonationLocationRequest(int explosionIndex) const { DetonationLocationRequest req; req.id(m_data.id); // m_data.id == AesItemId req.index(explosionIndex); req.chargeTypeId(m_data.chargeTypeId); // req.igniterTypeId(m_data.igniterTypeId); req.igniterTypeId(0); // req.igniterFunctionId(m_data.igniterFunctionId); req.igniterFunctionId(0); req.trajectory(m_trajectoryCont[explosionIndex].trajectoryGeometry); return req; } bool ObjectEffectCluster::setEffectors(const QVector &effectors) { // clear first but only when we need to update them m_effectorData.clear(); DEBUG_LOG(m_logger) << " Try assing effector from effector size " << effectors.size(); for (const auto &effectorData : effectors) { m_effectorData.insert(effectorData.participantId, effectorData); // update effector if (m_data.effectStateId > AreaEffectSimulation::AesEffectState::EsExpired) { DEBUG_LOG(m_logger) << " Try assing effector from effector " << effectorData.id << " pos x " << effectorData.mhpPosition.x() << " "; if (!assignEffector(effectorData.participantId)) { DEBUG_LOG(m_logger) << " Try assing effector from effector " << effectorData.id << " pos x " << effectorData.mhpPosition.x() << " FAILED"; return false; } if (!updateEffector(effectorData.participantId, effectorData)) { DEBUG_LOG(m_logger) << " Try assing effector from effector " << effectorData.id << " pos x " << effectorData.mhpPosition.x() << " FAILED2 "; return false; } } } return true; } void ObjectEffectCluster::setAesProperty(QJsonObject property, bool pubish) { QJsonObject propertiesObject = QJsonDocument::fromJson(m_data.properties.toUtf8()).object(); // same as QJsonObject.merge for (auto it = property.begin(); it != property.end(); ++it) propertiesObject.insert(it.key(), it.value()); QJsonDocument doc(propertiesObject); QByteArray propertiesArr = QJsonDocument(doc).toJson(QJsonDocument::Compact); m_data.properties = QString::fromUtf8(propertiesArr); if (pubish) onPublishData(); } QPolygonF ObjectEffectCluster::generateForm() { return Polygons::effectClusterArea(m_data); } bool ObjectEffectCluster::sendFireCommand(const QString &identifier) { // just in case we didn't consumed all effectors in the first place DEBUG_LOG(m_logger) << "!!!!!!!!!!!!!!! try send fire command to: " << identifier; const auto &participant = getParticipantFromInternalName(identifier); if (!m_effectorNameToIdMap.contains(identifier)) { if (participant.id() <= 0) return false; assignEffector(participant.id()); } if (!m_mainHandler->effectorHandler()) { ERROR_LOG(m_logger) << "No effector Handler"; return false; } auto effector = m_mainHandler->effectorHandler()->getObjectEffector(m_effectorNameToIdMap.value(identifier)); if (!effector) { ERROR_LOG(m_logger) << "No effector found for: " << identifier; return false; } if (effector->assignedAesItemId() > 0 && effector->assignedAesItemId() != m_data.id) { ERROR_LOG(m_logger) << "No effector found for participantId: " << m_effectorNameToIdMap.value(identifier); return false; } // else //if (effector->aesId() <= 0) //consume the effector for our effect cluster //{ // XKCD crashes at the time! // effector->assignAesItemRequest(m_data.id); // } // we got a fireCommand, meybe need to swtich to prepared if (m_data.effectStateId == AreaEffectSimulation::AesEffectState::EsCreated) { DEBUG_LOG(m_logger) << "move from recieved TO prepared: flightime: " << m_effectorData.value(participant.id()).flightTime << " currentTime:" << partitionTime().toString(); // Set Actication time to current + flight duration of the current effector DEBUG_LOG(m_logger) << "START STIME NEW: " << partitionTime().addSecs(m_effectorData.value(participant.id()).flightTime).toString(); m_data.effectStart = partitionTime().addSecs(m_effectorData.value(participant.id()).flightTime); if (!trySwitchToPrepared()) { WARNING_LOG(m_logger) << "Could not switch to prepared for aesid " << m_data.id; } } effector->fireOrderRequest(); DEBUG_LOG(m_logger) << "send: fireorder to " << m_effectorNameToIdMap.value(identifier); // now connect to chargeeffects connect(m_mainHandler->chargeEffectHandler().get(), &AreaEffectSimulation::HandlerChargeEffect::chargeEffectActive, this, &ObjectEffectCluster::onChargeEffectChanged); if (m_fireHoldWaitforLateFire) m_fireHoldWaitforLateFire->start(); // restart timer return true; } bool ObjectEffectCluster::sendFireCommand(int participantId) { // if (!m_effectorNameToIdMap.values().contains(participantId)) assignEffector(participantId); auto effector = m_mainHandler->effectorHandler()->getObjectEffector(participantId); if (!effector) { ERROR_LOG(m_logger) << "No effector found for participantId: " << participantId; return false; } if (effector->assignedAesItemId() > 0 && effector->assignedAesItemId() != m_data.id) { ERROR_LOG(m_logger) << "Got Fire Command but Effector for participantId: " << participantId << " is not assigned to our aes id: " << m_data.id << " vs " << effector->assignedAesItemId(); return false; } // else if (effector->getAssignedAesItemId() <= 0) //consume the effector for our effect cluster // effector->assignAesItemRequest(m_data.id); DEBUG_LOG(m_logger) << "Send FireCommand to Effector " << participantId; effector->fireOrderRequest(); // now connect to chargeeffects connect(m_mainHandler->chargeEffectHandler().get(), &AreaEffectSimulation::HandlerChargeEffect::chargeEffectActive, this, &ObjectEffectCluster::onChargeEffectChanged, Qt::UniqueConnection); return true; } bool ObjectEffectCluster::overrideEffectorStates(const AesEffectorState &newState) { if (!m_mainHandler->effectClusterHandler()) return false; switch (newState.underlying()) { case AesEffectorState::EsReadyToFire: { // Override internal state in case DMF is not ready yet m_data.effectorState = AesEffectorState::EsReadyToFire; DEBUG_LOG(m_logger) << "Set Ready to Fire !"; onPublishData(); if (m_readytoFire.size() == m_effectorData.count() && m_data.effectorState == AesEffectorState::EsReadyToFire) { // we need some time to handle the ack on tab QTimer::singleShot(1000, this, [this]() { DEBUG_LOG(m_logger) << "Everything is ready send Readiness now"; onSendReadinessToTab(); }); } const QJsonObject probObject = QJsonDocument::fromJson(QByteArray::fromStdString(m_data.properties.toStdString())).object(); FiringType firingType = FiringType::FireOnCommand; if (probObject.contains(AdlerJson::JsonKeys::ABFEUERUNGSART())) firingType = static_cast(probObject[AdlerJson::JsonKeys::ABFEUERUNGSART()].toInt()); if (m_data.effectSourceId == ProjectDataManagement::EffectSource::ES_ADLER || (m_data.effectSourceId == ProjectDataManagement::EffectSource::ES_ADLER_STANDALONE && firingType != FiringType::FireNow)) { return true; } // Will also start Fire Q_FALLTHROUGH(); } case AesEffectorState::EsReceivedOrder: { DEBUG_LOG(m_logger) << "Set Start !"; // got an early start if (m_data.effectStateId == AesEffectState::EsCreated) { // first try to set prepared m_data.effectStart = partitionTime().addMSecs(m_effectorData.begin().value().flightTime); DEBUG_LOG(m_logger) << "SWITCH TO PREPARED AND SET FIRING TIME REACHED"; m_firingTimeReached = true; trySwitchToPrepared(); } if (m_fireAutomaton) { DEBUG_LOG(m_logger) << "Set Start for fireAutomaton !"; return m_fireAutomaton->start(); } else { DEBUG_LOG(m_logger) << "Set Start for Manual Mode !"; for (const auto &effectorId : m_effectorData.keys()) { // Only fire if there is still a round left if (m_firedPerEfector.value(effectorId) < m_effectorData.value(effectorId).chargeAmount) { if (!sendFireCommand(effectorId)) { WARNING_LOG(m_logger) << "cant fire for effector! " << effectorId; return false; } } } } connect(m_mainHandler->chargeEffectHandler().get(), &AreaEffectSimulation::HandlerChargeEffect::chargeEffectActive, this, &ObjectEffectCluster::onChargeEffectChanged, Qt::UniqueConnection); return true; } default: break; } return false; } ParticipantManagement::Participant ObjectEffectCluster::getParticipantFromInternalName(const QString &name) { if (m_data.participantId <= 0) // No origin Unit return {}; const auto originator = m_mainHandler->dds()->ppmDtc()->participant(m_data.participantId); if (originator.name() == name.toStdString()) // in Case of a single originator and not a unit return the originator return originator; if (originator.id() <= 0 || originator.isDeleted()) // no Originator found or is deleted return {}; const auto originatorType = m_mainHandler->dds()->ppmDtc()->type(originator.typeId()); if (originatorType.typeClass() != ParticipantManagement::TypeClass::Unit) // not a unit return {}; // Now we look up for the inferior participants to find the specific efdfector // the return value is either the correct participant or empty return m_mainHandler->dds()->ppmDtc()->getParticipantByInternalName(originator.partyId(), QString::fromStdString(originator.name()), name); } bool ObjectEffectCluster::assignEffector(int participantId) { DEBUG_LOG(m_logger) << "TRY ASSIGN Effector " << participantId << ""; if (m_data.effectStateId == AesEffectState::EsExpired) return false; if (m_effectorNameToIdMap.values().contains(participantId)) { DEBUG_LOG(m_logger) << "Effector " << participantId << "Already assigned"; return true; } const auto &participant = m_mainHandler->dds()->ppmDtc()->participant(participantId); if (participant.id() <= 0) { ERROR_LOG(m_logger) << "TRY ASSIGN Effector " << participantId << " 2 no participant ID"; return false; } if (participant.name().empty() && participant.designator().empty()) { ERROR_LOG(m_logger) << "TRY ASSIGN Effector " << participantId << " 3 no names"; return false; } if (!m_mainHandler->effectorHandler()) { ERROR_LOG(m_logger) << "TRY ASSIGN Effector " << participantId << " 4 no effectorHandler"; return false; } auto effector = m_mainHandler->effectorHandler()->getObjectEffector(participantId); if (!effector) { ERROR_LOG(m_logger) << "TRY ASSIGN Effector " << participantId << " 5 no effector for participantId"; return false; } m_effectorNameToIdMap.insert(!participant.name().empty() ? QString::fromStdString(participant.name()) : QString::fromStdString(participant.name()), participant.id()); // just in case override timers with cadence const QJsonObject probObject = QJsonDocument::fromJson(QByteArray::fromStdString(m_data.properties.toStdString())).object(); FiringType firingType = FiringType::ReportReadyToFire; int cadence = 0; if (probObject.contains(AdlerJson::JsonKeys::ABFEUERUNGSART())) firingType = static_cast(probObject[AdlerJson::JsonKeys::ABFEUERUNGSART()].toInt()); if (m_data.effectSourceId != ProjectDataManagement::EffectSource::ES_ADLER && firingType == FiringType::FireOnCommand) { const auto &gearTypeIds = m_mainHandler->dds()->pdmDtc()->gearTypeIdFromChargeTypeId(m_data.chargeTypeId); cadence = 20000; // emergency fallback if (gearTypeIds.size()) cadence = m_mainHandler->dds()->pdmDtc()->cadence(gearTypeIds.at(0)); DEBUG_LOG(m_logger) << "Override with cadence " << cadence; } if (effector->assignedAesItemId() == m_data.id) { if (cadence > 0) effector->overrideReadynessTimerMinMax(cadence, cadence); return true; } connect(effector, &AreaEffectSimulation::ObjectEffector::signalFired, this, &ObjectEffectCluster::onEffectorFired, Qt::UniqueConnection); connect(effector, &AreaEffectSimulation::ObjectEffector::signalNotReadyToFire, this, &ObjectEffectCluster::onEffectorNotReadyToFire, Qt::UniqueConnection); connect(effector, &AreaEffectSimulation::ObjectEffector::signalReadyToFire, this, &ObjectEffectCluster::onEffectorReadyToFire, Qt::UniqueConnection); connect(effector, &AreaEffectSimulation::ObjectEffector::signalReceivedOrder, this, &ObjectEffectCluster::onEffectorReceivedOrder, Qt::UniqueConnection); connect(effector, &AreaEffectSimulation::ObjectEffector::signalReportedFired, this, &ObjectEffectCluster::onEffectorReportedFired, Qt::UniqueConnection); effector->assignAesItemRequest(m_data.id); if (cadence > 0) effector->overrideReadynessTimerMinMax(cadence, cadence); DEBUG_LOG(m_logger) << "Try assign Effector " << participantId << " complete!"; return true; } bool ObjectEffectCluster::updateEffector(int participantId, const Database::AesEffectorData &data) { auto effector = m_mainHandler->effectorHandler()->getObjectEffector(participantId); if (!effector) { ERROR_LOG(m_logger) << "No effector found for partId: " << participantId; return false; } effector->setEffectorData(fromDatabase(data)); DEBUG_LOG(m_logger) << "DONE Update effectordata for partId: " << participantId; return true; } void ObjectEffectCluster::freeEffectors() { for (const auto effectorId : m_effectorNameToIdMap) { auto effector = m_mainHandler->effectorHandler()->getObjectEffector(effectorId); // Only reset effector if it's still our own ? if (effector) { disconnect(effector); effector->resetReadynessTimer(); effector->deleteAssignmentAesItem(); DEBUG_LOG(m_logger) << "DONE free effector: " << effectorId; } } } QVector ObjectEffectCluster::generateMHPs(const QVector &effectorData, const AesExplosionGeometry &geometry) { double singleWidthOut, singleHeightOut; singleWidthOut = singleHeightOut = 0; QVector ret; const LtsCoordTrans::UtmCoordinate &impactPositionUtm = LtsCoordTrans::UtmCoordinate::fromGeographic(m_data.impactPosition); // impactPosition = cornerBottomLeft const Polygons::AreaBottomLeft areaBLOrgWithoutTla(impactPositionUtm, m_data.impactWidth, m_data.impactLength, m_data.rotationAngle); const Polygons::AreaBottomLeft areaBlWithTla = Polygons::explosionClusterAddTla(areaBLOrgWithoutTla, m_data.tla); // Radial extras const QPointF center = Polygons::bottomLeft2CenterPoint(m_data.impactPosition, m_data.impactWidth, m_data.impactLength, 0.0); const LtsCoordTrans::UtmCoordinate ¢erUtm = LtsCoordTrans::UtmCoordinate::fromGeographic(center, impactPositionUtm.zone); const LtsCoordTrans::UtmCoordinate &firingOriginUtm = LtsCoordTrans::UtmCoordinate::fromGeographic(m_data.firingCenterPosition.toPointF()); QVector mhps_asUtm; switch (geometry.underlying()) { case AesExplosionGeometry::EgLine: case AesExplosionGeometry::EgArea: { mhps_asUtm = Polygons::explosionClusterAreaToMtps(areaBlWithTla, m_data.mhpCountRequest, &singleWidthOut, &singleHeightOut); break; } case AesExplosionGeometry::EgPoint: case AesExplosionGeometry::EgAimPoint: { mhps_asUtm = Polygons::explosionClusterAreaToMtps(areaBlWithTla, 1, &singleWidthOut, &singleHeightOut); while (mhps_asUtm.size() < effectorData.size()) { // copy same mhps_asUtm.append(mhps_asUtm.at(0)); } break; } case AesExplosionGeometry::EgRadial: { mhps_asUtm = Polygons::explosionClusterRadialToMtps(areaBlWithTla, m_data.mhpCountRequest, firingOriginUtm, centerUtm, &singleWidthOut, &singleHeightOut); break; } default: break; } int mhpPos = 0; if (mhps_asUtm.size() != effectorData.size()) return ret; for (const auto &mhpPoint : mhps_asUtm) { auto effectorDataNew = effectorData.at(mhpPos); LtsCoordTrans::UtmCoordinate temp(impactPositionUtm.zone, mhpPoint.x(), mhpPoint.y()); effectorDataNew.mhpPosition.setX(temp.geographic().x()); effectorDataNew.mhpPosition.setY(temp.geographic().y()); effectorDataNew.mhpScatterWidth = singleWidthOut; effectorDataNew.mhpScatterLenght = singleHeightOut; mhpPos++; DEBUG_LOG(m_logger) << " append new EffectorData with point: " << temp.geographic().x() << " " << temp.geographic().y(); ret.append(effectorDataNew); } return ret; } void ObjectEffectCluster::onSendReadinessToTab() { DEBUG_LOG(m_logger) << "onSendReadinessToTab"; if (effectState() == AesEffectState::EsRemoved) return; if (m_data.effectSourceId != ProjectDataManagement::EffectSource::ES_ADLER) return; if (m_data.effectStateId != AesEffectState::EsCreated) return; // need properties now to know wich firingType is set const QJsonObject probObject = QJsonDocument::fromJson(QByteArray::fromStdString(m_data.properties.toStdString())).object(); FiringType firingType = FiringType::FireNow; if (probObject.contains(AdlerJson::JsonKeys::ABFEUERUNGSART())) firingType = static_cast(probObject[AdlerJson::JsonKeys::ABFEUERUNGSART()].toInt()); // clear ready to fire. Wait for all beeing ready again m_readytoFire.clear(); // We always send the first fireReadynessReport regardless of the Firing Type // Only send FireRediness after the initial one (at prep state) when we are in Mode // Fire on Command (K) DEBUG_LOG(m_logger) << "onSendReadinessToTab first Time" << m_firstTimeReady << " firing " << firingType << " currente State " << m_data.effectStateId; if (!m_firstTimeReady && firingType != FiringType::FireOnCommand) return; m_firstTimeReady = false; const QString reference = probObject[AdlerJson::JsonKeys::FEUERKAMPFREFERENZ()].toString(); TrainingAreaBase::SendExFireReadinessRequest request; request.reference() = reference.toStdString(); request.partyId() = m_data.partyId; request.participantId() = m_data.participantId; m_mainHandler->dds()->tabFtc()->sendExFireReadinessRequest(request, this); } void ObjectEffectCluster::onSendConfirmationReportToTab(int participantId) { if (m_data.effectSourceId != ProjectDataManagement::EffectSource::ES_ADLER) return; DEBUG_LOG(m_logger) << "onSendConfirmationReportToTab!"; const QJsonObject probObject = QJsonDocument::fromJson(QByteArray::fromStdString(m_data.properties.toStdString())).object(); const QString reference = probObject[AdlerJson::JsonKeys::FEUERKAMPFREFERENZ()].toString(); TrainingAreaBase::SendExConfirmationReportRequest request; request.reference() = reference.toStdString(); request.partyId() = m_data.partyId; request.unitIdentifier() = m_mainHandler->dds()->ppmDtc()->participant(m_data.participantId).name(); request.identifier() = m_mainHandler->dds()->ppmDtc()->participant(participantId).name(); m_mainHandler->dds()->tabFtc()->sendExConfirmationReportRequest(request, this); } void ObjectEffectCluster::onChargeEffectChanged(int chargeEffectId) { DEBUG_LOG(m_logger) << "Got chargeEffect Changed " << chargeEffectId << " looking for " << m_lookoutChargeEffectStart << " or " << m_lookoutChargeEffectEnd << " or " << m_lookoutChargeEffectPaused; if (chargeEffectId != m_lookoutChargeEffectStart && chargeEffectId != m_lookoutChargeEffectEnd && chargeEffectId != m_lookoutChargeEffectPaused) return; if (m_data.effectStateId != AreaEffectSimulation::AesEffectState::EsFired && m_data.effectStateId != AreaEffectSimulation::AesEffectState::EsPrepared && m_data.effectStateId != AreaEffectSimulation::AesEffectState::EsPaused && m_data.effectStateId != AreaEffectSimulation::AesEffectState::EsActivated) return; // in case of a Single Fire: just start the function again after a second (and a half) if ((m_lookoutChargeEffectEnd == m_lookoutChargeEffectStart && m_lookoutChargeEffectStart > 0) || (m_lookoutChargeEffectPaused == m_lookoutChargeEffectStart && m_lookoutChargeEffectStart > 0)) { DEBUG_LOG(m_logger) << "SingleFire stop next"; QTimer::singleShot(1500, this, [this, chargeEffectId]() { onChargeEffectChanged(chargeEffectId); }); } // In Case of Paused , Fired or Prepared: try to switch to active // Sometimes when the calcualted start time is faster than the impact the trychangestate might fail if (chargeEffectId == m_lookoutChargeEffectStart && (m_data.effectStateId == AreaEffectSimulation::AesEffectState::EsFired || m_data.effectStateId == AreaEffectSimulation::AesEffectState::EsPaused || m_data.effectStateId == AreaEffectSimulation::AesEffectState::EsPrepared)) { m_data.effectStart = partitionTime(); if (m_data.effectStateId != AreaEffectSimulation::AesEffectState::EsActivated && !trySwitchToActivated()) { DEBUG_LOG(m_logger) << "Could't change to active maybe already done! Current State: " << m_data.effectStateId; } m_lookoutChargeEffectStart = 0; return; } if (m_lookoutChargeEffectPaused == chargeEffectId && m_data.effectStateId == AreaEffectSimulation::AesEffectState::EsActivated) { // reduce effectDuration m_data.effectDuration = m_data.effectDuration - (partitionTimeMSec() - m_data.effectStart.toMSecsSinceEpoch()); DEBUG_LOG(m_logger) << "Last flying shot fired and landed, new duration: " << m_data.effectDuration; if (!trySwitchToPaused()) { DEBUG_LOG(m_logger) << "Could not switch to Paused, maybe alredy done. Current State: " << m_data.effectStateId; } m_lookoutChargeEffectPaused = 0; } // In Case of Executed: try to switch to active // Sometimes when the calcualted start time is faster than the impact the trychangestate might fail if (m_lookoutChargeEffectEnd == chargeEffectId && m_data.effectStateId == AreaEffectSimulation::AesEffectState::EsActivated) { // truncate effect duration to now m_data.effectDuration = partitionTimeMSec() - m_data.effectStart.toMSecsSinceEpoch(); DEBUG_LOG(m_logger) << "All shots fired and landed, new duration: " << m_data.effectDuration; if (!trySwitchToExpired()) { DEBUG_LOG(m_logger) << "Could not switch to Expired, maybe alredy done. Current State: " << m_data.effectStateId; } disconnect(m_mainHandler->chargeEffectHandler().get()); m_lookoutChargeEffectEnd = 0; } } } // Namespace AreaEffectSimulation