agrobot_base/AgroBase/OperationControl/Mapas/Creation/MapaCriacaoService.cs

845 lines
23 KiB
C#

using NetTopologySuite.Geometries;
using OperationControl.Mapas.Models;
using OperationControl.Mapas.Services;
using System.Globalization;
using static AgroBase.Models.Enums;
namespace OperationControl.Mapas.Creation
{
/// <summary>
/// Backend puro da criação de mapas por levantamento GNSS.
///
/// Não conhece WPF, Mapsui, Dock, Rover ou GpsService.
/// A futura camada Controller entrega snapshots GNSS válidos
/// e pede operações a este serviço.
/// </summary>
public sealed class MapaCriacaoService
{
private const double DistanciaMinimaPontoManualMetros = 0.02;
private readonly MapaValidationService _validationService;
private readonly GeometryFactory _geometryFactory =
new(
new PrecisionModel(),
4326
);
public MapaCriacaoService(
MapaValidationService? validationService = null)
{
_validationService =
validationService
?? new MapaValidationService();
}
public void Iniciar(
MapaEstadoCriacao estado,
string? nome,
TipoMapaOperacao tipo,
double distanciaMinimaAutomaticaMetros = 0.50)
{
ValidarEstado(estado);
if (estado.EmCriacao)
{
throw new InvalidOperationException(
"Já existe uma sessão de criação de mapa ativa."
);
}
if (
!double.IsFinite(distanciaMinimaAutomaticaMetros) ||
distanciaMinimaAutomaticaMetros <= 0
)
{
throw new ArgumentOutOfRangeException(
nameof(distanciaMinimaAutomaticaMetros),
"A distância mínima automática deve ser maior que zero."
);
}
estado.Limpar();
estado.EmCriacao = true;
estado.Nome = NormalizarNome(nome);
estado.Tipo = tipo;
estado.DistanciaMinimaCapturaAutomaticaMetros =
distanciaMinimaAutomaticaMetros;
estado.ModoCaptura = MapaModoCapturaCriacao.Manual;
estado.GravacaoAutomaticaAtiva = false;
estado.RuasMutaveis.Add(
new MapaRuaCriacao
{
Numero = 1
}
);
estado.IndiceRuaAtual = 0;
}
public void Cancelar(
MapaEstadoCriacao estado)
{
ValidarEstado(estado);
estado.Limpar();
}
public void DefinirNome(
MapaEstadoCriacao estado,
string? nome)
{
ValidarEstadoAtivo(estado);
estado.Nome = NormalizarNome(nome);
}
public bool DefinirModoCaptura(
MapaEstadoCriacao estado,
MapaModoCapturaCriacao modo,
out string motivo)
{
ValidarEstadoAtivo(estado);
motivo = string.Empty;
if (estado.GravacaoAutomaticaAtiva)
{
motivo =
"Finalize a gravação automática antes de trocar o modo.";
return false;
}
estado.ModoCaptura = modo;
return true;
}
public bool IniciarGravacaoAutomatica(
MapaEstadoCriacao estado,
out string motivo)
{
ValidarEstadoAtivo(estado);
motivo = string.Empty;
if (
estado.ModoCaptura !=
MapaModoCapturaCriacao.Automatico
)
{
motivo =
"A captura atual não está no modo automático.";
return false;
}
if (estado.GravacaoAutomaticaAtiva)
return true;
if (estado.RuaAtual == null)
{
motivo =
"Não existe rua atual para receber pontos.";
return false;
}
estado.GravacaoAutomaticaAtiva = true;
return true;
}
public bool PararGravacaoAutomatica(
MapaEstadoCriacao estado,
out string motivo)
{
ValidarEstadoAtivo(estado);
motivo = string.Empty;
estado.GravacaoAutomaticaAtiva = false;
return true;
}
public bool TentarAdicionarPontoManual(
MapaEstadoCriacao estado,
MapaPontoCriacao ponto,
out string motivo)
{
ValidarEstadoAtivo(estado);
motivo = string.Empty;
if (
estado.ModoCaptura !=
MapaModoCapturaCriacao.Manual
)
{
motivo =
"A captura atual está no modo automático.";
return false;
}
if (estado.GravacaoAutomaticaAtiva)
{
motivo =
"Finalize a gravação automática antes de registrar um ponto manual.";
return false;
}
return TentarAdicionarPonto(
estado,
ponto,
DistanciaMinimaPontoManualMetros,
out motivo
);
}
public bool TentarAdicionarPontoAutomatico(
MapaEstadoCriacao estado,
MapaPontoCriacao ponto,
bool forcarPrimeiroPonto,
out string motivo)
{
ValidarEstadoAtivo(estado);
motivo = string.Empty;
if (
estado.ModoCaptura !=
MapaModoCapturaCriacao.Automatico ||
!estado.GravacaoAutomaticaAtiva
)
{
motivo =
"A gravação automática não está ativa.";
return false;
}
MapaRuaCriacao? rua = estado.RuaAtual;
if (rua == null)
{
motivo =
"Não existe rua atual para receber pontos.";
return false;
}
if (
forcarPrimeiroPonto &&
rua.QuantidadePontos == 0
)
{
return TentarAdicionarPonto(
estado,
ponto,
distanciaMinimaMetros: 0,
out motivo
);
}
return TentarAdicionarPonto(
estado,
ponto,
estado.DistanciaMinimaCapturaAutomaticaMetros,
out motivo
);
}
public bool TentarRemoverUltimoPonto(
MapaEstadoCriacao estado,
out string motivo)
{
ValidarEstadoAtivo(estado);
motivo = string.Empty;
if (estado.GravacaoAutomaticaAtiva)
{
motivo =
"Finalize a gravação automática antes de remover pontos.";
return false;
}
MapaRuaCriacao? rua = estado.RuaAtual;
if (
rua == null ||
rua.QuantidadePontos <= 0
)
{
motivo =
"A rua atual não possui pontos.";
return false;
}
rua.PontosMutaveis.RemoveAt(
rua.QuantidadePontos - 1
);
return true;
}
public bool TentarCriarNovaRua(
MapaEstadoCriacao estado,
out string motivo)
{
ValidarEstadoAtivo(estado);
motivo = string.Empty;
if (estado.GravacaoAutomaticaAtiva)
{
motivo =
"Finalize a gravação automática antes de criar outra rua.";
return false;
}
MapaRuaCriacao? atual = estado.RuaAtual;
if (
atual == null ||
atual.QuantidadePontos < 2
)
{
int numero =
atual?.Numero
?? (estado.IndiceRuaAtual + 1);
motivo =
$"Registre pelo menos 2 pontos na rua {numero} antes de criar uma nova rua.";
return false;
}
estado.RuasMutaveis.Add(
new MapaRuaCriacao
{
Numero =
estado.RuasMutaveis.Count + 1
}
);
estado.IndiceRuaAtual =
estado.RuasMutaveis.Count - 1;
return true;
}
public bool TentarSelecionarRua(
MapaEstadoCriacao estado,
int indiceRua,
out string motivo)
{
ValidarEstadoAtivo(estado);
motivo = string.Empty;
if (estado.GravacaoAutomaticaAtiva)
{
motivo =
"Finalize a gravação automática antes de trocar de rua.";
return false;
}
if (
indiceRua < 0 ||
indiceRua >= estado.RuasMutaveis.Count
)
{
motivo =
"A rua informada não existe.";
return false;
}
estado.IndiceRuaAtual = indiceRua;
return true;
}
public bool TentarExcluirRua(
MapaEstadoCriacao estado,
int indiceRua,
out string motivo)
{
ValidarEstadoAtivo(estado);
motivo = string.Empty;
if (estado.GravacaoAutomaticaAtiva)
{
motivo =
"Finalize a gravação automática antes de excluir uma rua.";
return false;
}
if (
indiceRua < 0 ||
indiceRua >= estado.RuasMutaveis.Count
)
{
motivo =
"A rua informada não existe.";
return false;
}
/*
* A sessão mantém pelo menos uma rua.
* Se existe apenas uma, excluir significa limpar a Rua 1.
*/
if (estado.RuasMutaveis.Count == 1)
{
estado.RuasMutaveis[0]
.PontosMutaveis
.Clear();
estado.IndiceRuaAtual = 0;
return true;
}
estado.RuasMutaveis.RemoveAt(indiceRua);
RenumerarRuas(estado);
if (
estado.IndiceRuaAtual >=
estado.RuasMutaveis.Count
)
{
estado.IndiceRuaAtual =
estado.RuasMutaveis.Count - 1;
}
else if (indiceRua < estado.IndiceRuaAtual)
{
estado.IndiceRuaAtual--;
}
return true;
}
public void LimparPontos(
MapaEstadoCriacao estado)
{
ValidarEstadoAtivo(estado);
if (estado.GravacaoAutomaticaAtiva)
{
throw new InvalidOperationException(
"Finalize a gravação automática antes de limpar o levantamento."
);
}
estado.RuasMutaveis.Clear();
estado.RuasMutaveis.Add(
new MapaRuaCriacao
{
Numero = 1
}
);
estado.IndiceRuaAtual = 0;
}
public MapaResumoCriacao CriarResumo(
MapaEstadoCriacao estado)
{
ValidarEstado(estado);
MapaRuaCriacao? atual = estado.RuaAtual;
double comprimentoAtual =
atual == null
? 0
: CalcularComprimentoMetros(
atual.Pontos
);
double comprimentoTotal =
estado.Ruas.Sum(
rua =>
CalcularComprimentoMetros(
rua.Pontos
)
);
var ruasResumo =
estado.Ruas
.Select(
(rua, indice) =>
new MapaRuaResumoCriacao
{
Numero = rua.Numero,
QuantidadePontos = rua.QuantidadePontos,
ComprimentoMetros =
CalcularComprimentoMetros(rua.Pontos),
Valida = rua.Valida,
Atual = indice == estado.IndiceRuaAtual
}
)
.ToList();
return new MapaResumoCriacao
{
EmCriacao = estado.EmCriacao,
Nome = estado.Nome,
RuaAtual = atual?.Numero ?? 0,
QuantidadeRuas = estado.QuantidadeRuas,
QuantidadeRuasValidas = estado.QuantidadeRuasValidas,
PontosRuaAtual = atual?.QuantidadePontos ?? 0,
PontosTotal = estado.QuantidadePontosTotal,
ComprimentoRuaAtualMetros = comprimentoAtual,
ComprimentoTotalMetros = comprimentoTotal,
ModoCaptura = estado.ModoCaptura,
GravacaoAutomaticaAtiva = estado.GravacaoAutomaticaAtiva,
DistanciaMinimaAutomaticaMetros =
estado.DistanciaMinimaCapturaAutomaticaMetros,
PodeNovaRua = estado.PodeCriarNovaRua,
PodeSalvar =
estado.PodeSalvar &&
!string.IsNullOrWhiteSpace(estado.Nome),
Ruas = ruasResumo
};
}
/// <summary>
/// Converte o levantamento transitório para o domínio canônico.
/// IDs finais são sempre strings numéricas sequenciais.
/// </summary>
public MapaDocumento CriarDocumento(
MapaEstadoCriacao estado)
{
ValidarEstadoAtivo(estado);
if (estado.GravacaoAutomaticaAtiva)
{
throw new InvalidOperationException(
"Finalize a gravação automática antes de gerar o mapa."
);
}
string nome = NormalizarNome(estado.Nome);
if (string.IsNullOrWhiteSpace(nome))
{
throw new InvalidOperationException(
"Informe um nome para o novo mapa."
);
}
if (estado.Ruas.Count == 0)
{
throw new InvalidOperationException(
"O levantamento não possui ruas."
);
}
for (int i = 0; i < estado.Ruas.Count; i++)
{
MapaRuaCriacao rua = estado.Ruas[i];
if (
rua == null ||
rua.QuantidadePontos < 2
)
{
throw new InvalidOperationException(
$"A rua {i + 1} precisa possuir pelo menos 2 pontos."
);
}
}
var documento =
new MapaDocumento
{
Nome = nome,
CaminhoOrigem = null,
FormatoOrigem = MapaFormatoOrigem.Desconhecido,
Tipo = estado.Tipo,
Modificado = true
};
for (int i = 0; i < estado.Ruas.Count; i++)
{
MapaRuaCriacao ruaCriacao = estado.Ruas[i];
string id =
(i + 1).ToString(
CultureInfo.InvariantCulture
);
Coordinate[] coordenadas =
ruaCriacao.Pontos
.Select(
ponto =>
new Coordinate(
ponto.Longitude,
ponto.Latitude
)
)
.ToArray();
LineString geometria =
_geometryFactory.CreateLineString(
coordenadas
);
documento.Ruas.Add(
new MapaRua
{
Id = id,
IdOriginal = null,
IdGeradoAutomaticamente = true,
Nome = nome,
Geometria = geometria,
ComprimentoMetros =
CalcularComprimentoMetros(
ruaCriacao.Pontos
),
Dist1 = 0,
Dist2 = 0,
IndiceFeatureOrigem = i
}
);
}
MapaResultadoValidacao validacao =
_validationService.Validar(
documento
);
if (!validacao.Valido)
{
string erros =
string.Join(
Environment.NewLine,
validacao.Erros
);
throw new InvalidOperationException(
"O mapa criado não passou pela validação canônica:" +
Environment.NewLine +
erros
);
}
return documento;
}
public static double CalcularComprimentoMetros(
IReadOnlyList<MapaPontoCriacao> pontos)
{
if (
pontos == null ||
pontos.Count < 2
)
{
return 0;
}
double total = 0;
for (int i = 1; i < pontos.Count; i++)
{
total += CalcularDistanciaMetros(
pontos[i - 1],
pontos[i]
);
}
return total;
}
public static double CalcularDistanciaMetros(
MapaPontoCriacao a,
MapaPontoCriacao b)
{
if (
a == null ||
b == null
)
{
return double.PositiveInfinity;
}
const double raioTerraMetros = 6378137.0;
double lat1 = GrausParaRadianos(a.Latitude);
double lat2 = GrausParaRadianos(b.Latitude);
double dLat = GrausParaRadianos(b.Latitude - a.Latitude);
double dLon = GrausParaRadianos(b.Longitude - a.Longitude);
double h =
Math.Sin(dLat / 2.0) * Math.Sin(dLat / 2.0) +
Math.Cos(lat1) *
Math.Cos(lat2) *
Math.Sin(dLon / 2.0) * Math.Sin(dLon / 2.0);
h = Math.Clamp(h, 0.0, 1.0);
return
2.0 *
raioTerraMetros *
Math.Atan2(
Math.Sqrt(h),
Math.Sqrt(1.0 - h)
);
}
private static bool TentarAdicionarPonto(
MapaEstadoCriacao estado,
MapaPontoCriacao ponto,
double distanciaMinimaMetros,
out string motivo)
{
motivo = string.Empty;
if (ponto == null)
{
motivo =
"O ponto informado é nulo.";
return false;
}
if (
!CoordenadaValida(
ponto.Latitude,
ponto.Longitude
)
)
{
motivo =
"O ponto informado possui coordenada inválida.";
return false;
}
MapaRuaCriacao? rua = estado.RuaAtual;
if (rua == null)
{
motivo =
"Não existe rua atual para receber pontos.";
return false;
}
if (
rua.QuantidadePontos > 0 &&
distanciaMinimaMetros > 0
)
{
MapaPontoCriacao ultimo =
rua.Pontos[
rua.QuantidadePontos - 1
];
double distancia =
CalcularDistanciaMetros(
ultimo,
ponto
);
if (
!double.IsFinite(distancia) ||
distancia < distanciaMinimaMetros
)
{
motivo =
"Ponto ignorado: distância do último ponto = " +
$"{distancia:F3} m; mínimo = " +
$"{distanciaMinimaMetros:F3} m.";
return false;
}
}
/*
* O estado guarda um snapshot próprio.
* Nenhuma referência externa fica viva aqui dentro.
*/
rua.PontosMutaveis.Add(
ponto.Clonar()
);
return true;
}
private static bool CoordenadaValida(
double latitude,
double longitude)
{
return
double.IsFinite(latitude) &&
double.IsFinite(longitude) &&
latitude >= -90.0 &&
latitude <= 90.0 &&
longitude >= -180.0 &&
longitude <= 180.0 &&
(
Math.Abs(latitude) > 1e-12 ||
Math.Abs(longitude) > 1e-12
);
}
private static void RenumerarRuas(
MapaEstadoCriacao estado)
{
for (
int i = 0;
i < estado.RuasMutaveis.Count;
i++
)
{
estado.RuasMutaveis[i].Numero = i + 1;
}
}
private static string NormalizarNome(
string? nome)
{
return
string.IsNullOrWhiteSpace(nome)
? string.Empty
: nome.Trim();
}
private static double GrausParaRadianos(
double graus)
{
return
graus *
Math.PI /
180.0;
}
private static void ValidarEstado(
MapaEstadoCriacao estado)
{
if (estado == null)
{
throw new ArgumentNullException(
nameof(estado)
);
}
}
private static void ValidarEstadoAtivo(
MapaEstadoCriacao estado)
{
ValidarEstado(estado);
if (!estado.EmCriacao)
{
throw new InvalidOperationException(
"Não existe uma sessão de criação de mapa ativa."
);
}
}
}
}