agrobot_base/AgroBase/OperationControl/Mapas/Controller/MapaOperacaoController.Bord...

1223 lines
30 KiB
C#

using AgroBase.Models;
using Mapsui.Layers;
using NetTopologySuite.Geometries;
using OperationControl.Controls;
using OperationControl.Mapas.Editing;
using OperationControl.Mapas.Rendering;
using OperationControl.Mapas.Services;
using System.Diagnostics;
using static AgroBase.Models.Enums;
namespace OperationControl.Mapas
{
public sealed partial class MapaOperacaoController
{
private const double
IdadeMaximaPosicaoBordaGnssSegundos =
2.0;
private const double
DistanciaMinimaPontoManualMetros =
0.02;
private int _capturaAutomaticaUiAgendada;
private readonly object
_snapshotGnssLocalLock =
new();
private GPSModel?
_ultimaLeituraGnssLocal;
private readonly
MapaBordaGnssRenderService
_bordaGnssRenderService =
new();
private readonly Dictionary<
MapViewControl,
MemoryLayer>
_layersBordaGnss =
new();
/*
* =========================================================
* SNAPSHOT DO GNSS LOCAL
* =========================================================
*/
private void AtualizarSnapshotGnssLocal(GPSModel dados)
{
if (dados == null)
return;
GPSModel copia = dados.Clone();
lock (_snapshotGnssLocalLock)
{
_ultimaLeituraGnssLocal = copia;
}
AgendarCapturaAutomaticaSeNecessario();
}
private GPSModel? ObterSnapshotGnssLocal()
{
lock (_snapshotGnssLocalLock)
{
return
_ultimaLeituraGnssLocal?
.Clone();
}
}
/*
* =========================================================
* RESUMO PARA O COCKPIT
* =========================================================
*/
private MapaResumoBordaGnss CriarResumoBordaGnss()
{
GPSModel? leitura =
ObterSnapshotGnssLocal();
bool leituraValida =
TentarValidarLeituraGnss(
leitura,
out double idadeSegundos,
out string motivo
);
bool qualidadeAlta =
leitura != null &&
(
leitura.QualidadeFix ==
TiposCorrecaoGPS.RTKFixo
||
leitura.QualidadeFix ==
TiposCorrecaoGPS.BaseFix
);
string status;
if (!leituraValida)
{
status =
motivo;
}
else
{
status =
$"{DescreverFix(leitura!.QualidadeFix)}" +
$" • {leitura.NumeroSatelites} sat" +
$" • {idadeSegundos:F1} s";
if (
double.IsFinite(
leitura.PrecisaoCm
)
)
{
status +=
$" • ~{leitura.PrecisaoCm:F1} cm";
}
}
return new MapaResumoBordaGnss
{
Modo = _estadoRecorte.ModoCapturaGnss,
QuantidadePontos = _estadoRecorte.QuantidadePontos,
ComprimentoMetros = CalcularComprimentoBordaGnss(),
CapturaAtiva = _estadoRecorte.CapturaAtiva,
Finalizada = _estadoRecorte.BordaGnssFinalizada,
LeituraGnssValida = leituraValida,
QualidadeAlta = qualidadeAlta,
StatusGnss = status,
GravacaoAutomaticaAtiva = _estadoRecorte.GravacaoAutomaticaAtiva,
DistanciaMinimaAutomaticaMetros = _estadoRecorte.DistanciaMinimaCapturaAutomaticaMetros,
};
}
private bool TentarValidarLeituraGnss(GPSModel? leitura, out double idadeSegundos, out string motivo)
{
idadeSegundos =
double.PositiveInfinity;
if (leitura == null)
{
motivo =
"GNSS sem leitura.";
return false;
}
if (!leitura.Inicializado)
{
motivo =
"GNSS ainda não inicializado.";
return false;
}
if (
!double.IsFinite(
leitura.Latitude
) ||
!double.IsFinite(
leitura.Longitude
) ||
leitura.Latitude < -90.0 ||
leitura.Latitude > 90.0 ||
leitura.Longitude < -180.0 ||
leitura.Longitude > 180.0 ||
(
Math.Abs(
leitura.Latitude
) < 1e-12 &&
Math.Abs(
leitura.Longitude
) < 1e-12
)
)
{
motivo =
"GNSS sem posição válida.";
return false;
}
double agoraMono =
Stopwatch.GetTimestamp() /
(double)
Stopwatch.Frequency;
double timestampPos =
leitura
.TimestampPos?
.valor
?? 0;
if (timestampPos > 0)
{
idadeSegundos =
Math.Max(
0,
agoraMono -
timestampPos
);
}
else if (
leitura.Momento !=
default
)
{
idadeSegundos =
Math.Max(
0,
(
DateTime.Now -
leitura.Momento
)
.TotalSeconds
);
}
if (
!double.IsFinite(
idadeSegundos
) ||
idadeSegundos >
IdadeMaximaPosicaoBordaGnssSegundos
)
{
motivo =
double.IsFinite(
idadeSegundos
)
? $"Posição GNSS desatualizada ({idadeSegundos:F1} s)."
: "Não foi possível determinar a idade da posição.";
return false;
}
motivo =
string.Empty;
return true;
}
private static string DescreverFix(TiposCorrecaoGPS fix)
{
return fix switch
{
TiposCorrecaoGPS.RTKFixo =>
"RTK FIX",
TiposCorrecaoGPS.RTKFlutuante =>
"RTK FLOAT",
TiposCorrecaoGPS.DGPS =>
"DGPS",
TiposCorrecaoGPS.BaseFix =>
"BASE FIX",
TiposCorrecaoGPS.Autonomo =>
"AUTÔNOMO",
TiposCorrecaoGPS.SemCorrecao =>
"SEM CORREÇÃO",
_ =>
fix.ToString()
};
}
/*
* =========================================================
* CAPTURA
* =========================================================
*/
private void RegistrarPontoBordaGnssManual()
{
if (
!_estadoEdicao.EmEdicao ||
_ferramentaEdicaoAtual !=
MapaFerramentaEdicao
.RecorteBordaGnss ||
!_estadoRecorte.CapturaAtiva
)
{
return;
}
GPSModel? leitura =
ObterSnapshotGnssLocal();
if (
!TentarValidarLeituraGnss(
leitura,
out double idade,
out string motivo
)
)
{
Log(
"[MAPA][EDICAO][BORDA-GNSS][PONTO][IGNORADO] " +
motivo
);
SincronizarCockpitEdicao();
return;
}
var ponto =
new Coordinate(
leitura!.Longitude,
leitura.Latitude
);
/*
* Manual respeita explicitamente
* a vontade do operador.
*
* Só bloqueamos duplicata praticamente
* idêntica.
*/
if (
_estadoRecorte.Pontos.Count > 0
)
{
Coordinate ultimo =
_estadoRecorte
.Pontos[
_estadoRecorte
.Pontos
.Count - 1
];
var linhaTeste =
new LineString(
new[]
{
new Coordinate(
ultimo.X,
ultimo.Y
),
new Coordinate(
ponto.X,
ponto.Y
)
}
);
double distancia =
MapaRecorteService
.CalcularComprimentoMetros(
linhaTeste
);
if (
distancia <
DistanciaMinimaPontoManualMetros
)
{
Log(
"[MAPA][EDICAO][BORDA-GNSS][PONTO][IGNORADO] " +
$"Duplicata | Dist={distancia:F3} m"
);
return;
}
}
_estadoRecorte
.AdicionarPontoBordaGnss(
ponto
);
AtualizarVisualBordaGnss();
SincronizarCockpitEdicao();
EstadoEdicaoAlterado?
.Invoke();
Log(
"[MAPA][EDICAO][BORDA-GNSS][PONTO] " +
$"N={_estadoRecorte.QuantidadePontos} | " +
$"Lat={leitura.Latitude:F9} | " +
$"Lon={leitura.Longitude:F9} | " +
$"Fix={leitura.QualidadeFix} | " +
$"Idade={idade:F2}s"
);
}
private void RemoverUltimoPontoBordaGnss()
{
if (
!_estadoRecorte
.RemoverUltimoPontoBordaGnss()
)
{
return;
}
AtualizarVisualBordaGnss();
SincronizarCockpitEdicao();
EstadoEdicaoAlterado?
.Invoke();
Log(
"[MAPA][EDICAO][BORDA-GNSS] " +
"Último ponto removido."
);
}
private void FinalizarCapturaBordaGnss()
{
if (
!_estadoRecorte
.FinalizarBordaGnss()
)
{
return;
}
AtualizarVisualBordaGnss();
SincronizarCockpitEdicao();
EstadoEdicaoAlterado?
.Invoke();
Log(
"[MAPA][EDICAO][BORDA-GNSS] " +
$"Captura finalizada | " +
$"Pontos={_estadoRecorte.QuantidadePontos} | " +
$"Comprimento={CalcularComprimentoBordaGnss():F2} m"
);
}
private void ContinuarCapturaBordaGnss()
{
if (
!_estadoRecorte
.ContinuarBordaGnss()
)
{
return;
}
SincronizarCockpitEdicao();
Log(
"[MAPA][EDICAO][BORDA-GNSS] " +
"Captura reaberta."
);
}
private void DescartarCapturaBordaGnss()
{
_estadoRecorte
.DescartarBordaGnss();
AtualizarVisualBordaGnss();
SincronizarCockpitEdicao();
EstadoEdicaoAlterado?
.Invoke();
Log(
"[MAPA][EDICAO][BORDA-GNSS] " +
"Captura descartada."
);
}
private double CalcularComprimentoBordaGnss()
{
if (
_estadoRecorte
.Pontos
.Count < 2
)
{
return 0;
}
var linha =
new LineString(
_estadoRecorte
.Pontos
.Select(
p =>
new Coordinate(
p.X,
p.Y
)
)
.ToArray()
);
return
MapaRecorteService
.CalcularComprimentoMetros(
linha
);
}
/*
* =========================================================
* RENDER
* =========================================================
*/
private void AtualizarVisualBordaGnss()
{
foreach (
MapViewControl mapa
in _mapasEmEdicao
.ToArray())
{
RenderizarBordaGnssNoMapa(
mapa
);
mapa
.Mapa
.Refresh();
}
}
private void AplicarBordaGnss()
{
if (
!_estadoEdicao.EmEdicao ||
_ferramentaEdicaoAtual !=
MapaFerramentaEdicao
.RecorteBordaGnss
)
{
return;
}
/*
* A captura precisa estar finalizada.
*
* Nada de cortar enquanto o operador
* ainda estiver levantando a borda.
*/
if (
!_estadoRecorte
.BordaGnssFinalizada ||
!_estadoRecorte
.PodeConcluirLinha
)
{
Log(
"[MAPA][EDICAO][BORDA-GNSS][APLICAR][IGNORADO] " +
"A borda ainda não foi finalizada."
);
return;
}
/*
* Escopo cirúrgico:
*
* Somente selecionadas exige pelo menos
* uma rua selecionada.
*/
if (
_estadoRecorte
.SomenteSelecionadas &&
!_estadoEdicao
.PossuiSelecao
)
{
Log(
"[MAPA][EDICAO][BORDA-GNSS][APLICAR][IGNORADO] " +
"Escopo=SomenteSelecionadas | " +
"Nenhuma rua selecionada."
);
SincronizarCockpitEdicao();
return;
}
/*
* Copiamos a geometria.
*
* O service não recebe a coleção interna
* mutável do estado.
*/
Coordinate[] pontos =
_estadoRecorte
.Pontos
.Select(
p =>
new Coordinate(
p.X,
p.Y
)
)
.ToArray();
try
{
MapaResultadoRecorte resultado =
_edicaoService
.ApararPorLinha(
_estadoEdicao,
pontos,
somenteSelecionadas:
_estadoRecorte
.SomenteSelecionadas
);
/*
* Nenhuma alteração:
*
* NÃO descartamos a borda.
*
* Isso é muito importante em campo.
* O operador ainda pode olhar a linha,
* clicar em Continuar e corrigir/adicionar
* novos pontos.
*/
if (!resultado.HouveAlteracao)
{
Log(
"[MAPA][EDICAO][BORDA-GNSS][APLICAR] " +
$"Escopo={_estadoRecorte.Escopo} | " +
resultado
);
SincronizarCockpitEdicao();
return;
}
/*
* Deu certo.
*
* A operação inteira já entrou no histórico
* como UMA única transação pelo
* MapaEdicaoService.
*
* Consumimos agora a borda utilizada e
* deixamos a ferramenta pronta para um
* novo levantamento.
*/
_estadoRecorte
.DescartarBordaGnss();
/*
* Houve alteração geométrica.
*
* Precisamos reconstruir o render
* do DocumentoTrabalho.
*/
RenderizarDocumentoEdicaoAtual();
SincronizarCockpitEdicao();
EstadoEdicaoAlterado?
.Invoke();
Log(
"[MAPA][EDICAO][BORDA-GNSS][APLICAR] " +
$"Escopo={_estadoRecorte.Escopo} | " +
resultado +
$" | Undo={PodeDesfazerEdicao} | " +
$"Redo={PodeRefazerEdicao}"
);
}
catch (Exception ex)
{
/*
* MapaEdicaoService é transacional.
*
* Em erro a geometria volta para o
* snapshot anterior.
*
* A borda GNSS permanece na tela para
* o operador não perder o levantamento.
*/
Log(
"[MAPA][EDICAO][BORDA-GNSS][APLICAR][ERRO] " +
ex.Message
);
SincronizarCockpitEdicao();
}
}
private void RenderizarBordaGnssNoMapa(MapViewControl mapaControl)
{
if (
mapaControl?.Mapa?.Map == null
)
{
return;
}
bool exibir =
_estadoEdicao.EmEdicao &&
_mapasEmEdicao.Contains(
mapaControl
) &&
_ferramentaEdicaoAtual ==
MapaFerramentaEdicao
.RecorteBordaGnss &&
_estadoRecorte.Tipo ==
MapaTipoRecorte
.BordaGnss;
if (
!_layersBordaGnss
.TryGetValue(
mapaControl,
out MemoryLayer? camada
)
)
{
camada =
_bordaGnssRenderService
.CriarCamada(
_estadoRecorte
);
mapaControl
.Mapa
.Map
.Layers
.Add(
camada
);
_layersBordaGnss[
mapaControl
] =
camada;
}
if (exibir)
{
_bordaGnssRenderService
.AtualizarCamada(
camada,
_estadoRecorte
);
}
else
{
camada.Features =
Array.Empty<
Mapsui.IFeature>();
}
/*
* Preview fica sobre o mapa,
* mas Rover/Base continuam no topo.
*/
var layers =
mapaControl
.Mapa
.Map
.Layers;
if (
layers.Contains(
camada
)
)
{
layers.Remove(
camada
);
layers.Add(
camada
);
}
GarantirEntidadesNoTopo(
mapaControl
);
}
private void DefinirModoCapturaBordaGnss(MapaModoCapturaGnss modo)
{
if (
!_estadoEdicao.EmEdicao ||
_ferramentaEdicaoAtual !=
MapaFerramentaEdicao
.RecorteBordaGnss
)
{
return;
}
if (
!_estadoRecorte
.DefinirModoCapturaGnss(
modo
)
)
{
return;
}
SincronizarCockpitEdicao();
Log(
"[MAPA][EDICAO][BORDA-GNSS][MODO] " +
$"Atual={modo}"
);
}
private void IniciarGravacaoBordaGnssAutomatica()
{
if (
!_estadoEdicao.EmEdicao ||
_ferramentaEdicaoAtual !=
MapaFerramentaEdicao
.RecorteBordaGnss
)
{
return;
}
GPSModel? leitura =
ObterSnapshotGnssLocal();
if (
!TentarValidarLeituraGnss(
leitura,
out double idade,
out string motivo
)
)
{
Log(
"[MAPA][EDICAO][BORDA-GNSS][AUTO][INICIO][IGNORADO] " +
motivo
);
SincronizarCockpitEdicao();
return;
}
if (
!_estadoRecorte
.IniciarGravacaoAutomatica()
)
{
return;
}
/*
* Registra imediatamente a posição
* onde o operador apertou INICIAR.
*/
ProcessarPontoAutomatico(
leitura!,
forcarPrimeiroPonto: true
);
SincronizarCockpitEdicao();
Log(
"[MAPA][EDICAO][BORDA-GNSS][AUTO] " +
"Gravação iniciada | " +
$"DistMin=" +
$"{_estadoRecorte.DistanciaMinimaCapturaAutomaticaMetros:F2} m | " +
$"Fix={leitura!.QualidadeFix} | " +
$"Idade={idade:F2}s"
);
}
private void ProcessarPontoAutomatico(GPSModel leitura, bool forcarPrimeiroPonto = false)
{
if (
leitura == null ||
!_estadoEdicao.EmEdicao ||
_ferramentaEdicaoAtual !=
MapaFerramentaEdicao
.RecorteBordaGnss ||
!_estadoRecorte
.GravacaoAutomaticaAtiva
)
{
return;
}
if (
!TentarValidarLeituraGnss(
leitura,
out double idade,
out _
)
)
{
return;
}
var ponto =
new Coordinate(
leitura.Longitude,
leitura.Latitude
);
bool adicionar =
_estadoRecorte
.QuantidadePontos == 0;
double distancia =
0;
if (
!adicionar &&
_estadoRecorte
.QuantidadePontos > 0
)
{
Coordinate ultimo =
_estadoRecorte
.Pontos[
_estadoRecorte
.QuantidadePontos - 1
];
var trecho =
new LineString(
new[]
{
new Coordinate(
ultimo.X,
ultimo.Y
),
new Coordinate(
ponto.X,
ponto.Y
)
}
);
distancia =
MapaRecorteService
.CalcularComprimentoMetros(
trecho
);
adicionar =
distancia >=
_estadoRecorte
.DistanciaMinimaCapturaAutomaticaMetros;
}
if (
forcarPrimeiroPonto &&
_estadoRecorte.QuantidadePontos == 0
)
{
adicionar =
true;
}
if (!adicionar)
return;
_estadoRecorte
.AdicionarPontoBordaGnss(
ponto
);
AtualizarVisualBordaGnss();
SincronizarCockpitEdicao();
EstadoEdicaoAlterado?
.Invoke();
Log(
"[MAPA][EDICAO][BORDA-GNSS][AUTO][PONTO] " +
$"N={_estadoRecorte.QuantidadePontos} | " +
$"Dist={distancia:F2} m | " +
$"Lat={leitura.Latitude:F9} | " +
$"Lon={leitura.Longitude:F9} | " +
$"Fix={leitura.QualidadeFix} | " +
$"Idade={idade:F2}s"
);
}
private void AgendarCapturaAutomaticaSeNecessario()
{
if (
!_estadoEdicao.EmEdicao ||
_ferramentaEdicaoAtual !=
MapaFerramentaEdicao
.RecorteBordaGnss ||
!_estadoRecorte
.GravacaoAutomaticaAtiva
)
{
return;
}
/*
* Evita colocar 300 delegates na fila
* caso o GNSS esteja publicando rápido.
*/
if (
System.Threading
.Interlocked
.Exchange(
ref _capturaAutomaticaUiAgendada,
1
) != 0
)
{
return;
}
var dispatcher =
System.Windows
.Application
.Current?
.Dispatcher;
if (dispatcher == null)
{
System.Threading
.Interlocked
.Exchange(
ref _capturaAutomaticaUiAgendada,
0
);
return;
}
dispatcher.BeginInvoke(
new Action(
() =>
{
try
{
if (
!_estadoEdicao.EmEdicao ||
_ferramentaEdicaoAtual !=
MapaFerramentaEdicao
.RecorteBordaGnss ||
!_estadoRecorte
.GravacaoAutomaticaAtiva
)
{
return;
}
GPSModel? leitura =
ObterSnapshotGnssLocal();
if (leitura == null)
return;
ProcessarPontoAutomatico(
leitura
);
}
finally
{
System.Threading
.Interlocked
.Exchange(
ref _capturaAutomaticaUiAgendada,
0
);
}
}
),
System.Windows.Threading
.DispatcherPriority.Background
);
}
}
}