agrobot_base/AgroBase/OperationControl/Mapas/Services/MapaEstimativaOperacaoServi...

752 lines
18 KiB
C#

using NetTopologySuite.Geometries;
using NetTopologySuite.LinearReferencing;
using OperationControl.Mapas.Models;
namespace OperationControl.Mapas.Services
{
public sealed class MapaEstimativaOperacaoService
{
/*
* 21 pontos distribuídos na parte útil
* da rua dão uma leitura boa do espaçamento,
* sem transformar cada clique em um processamento GIS pesado.
*/
private const int QuantidadeAmostrasLargura = 21;
/*
* Cache válido enquanto o Documento oficial
* continuar sendo a mesma instância.
*/
private MapaDocumento?
_documentoCache;
private readonly Dictionary<
string,
MapaEstimativaCorredor>
_cacheCorredores =
new(
StringComparer.OrdinalIgnoreCase
);
public MapaEstimativaOperacao Calcular(
MapaDocumento? documento,
MapaSelecaoOperacao? selecao,
double? velocidadeSemErvasMs = null,
double? velocidadeComErvasMs = null,
double percentualErvas = 0)
{
int quantidadeSelecionadas =
selecao?.Quantidade ?? 0;
var resultado =
new MapaEstimativaOperacao
{
QuantidadeRuasSelecionadas =
quantidadeSelecionadas,
PercentualErvasConsiderado =
Math.Clamp(
percentualErvas,
0,
100
)
};
if (
documento == null ||
selecao == null ||
quantidadeSelecionadas < 2
)
{
resultado.MotivoIndisponivel =
"Selecione ao menos duas ruas.";
return resultado;
}
GarantirCacheDocumento(
documento
);
var ruasPorId =
documento
.Ruas
.Where(
x =>
x != null &&
!string.IsNullOrWhiteSpace(
x.Id
)
)
.ToDictionary(
x => x.Id,
x => x,
StringComparer.OrdinalIgnoreCase
);
/*
* A ordem da seleção é a ordem operacional.
*
* Rua 0 + Rua 1 = corredor 0
* Rua 1 + Rua 2 = corredor 1
* etc.
*/
var ids =
selecao
.RuasOrdenadas
.ToList();
for (
int i = 0;
i < ids.Count - 1;
i++)
{
string idA =
ids[i];
string idB =
ids[i + 1];
if (
!ruasPorId.TryGetValue(
idA,
out var ruaA
) ||
!ruasPorId.TryGetValue(
idB,
out var ruaB
)
)
{
throw new InvalidOperationException(
$"Não foi possível montar " +
$"o corredor {idA} -> {idB}."
);
}
var corredor =
ObterCorredor(
documento,
ruaA,
ruaB
);
resultado
.Corredores
.Add(
corredor
);
}
resultado.DistanciaMetros =
resultado
.Corredores
.Sum(
x =>
x.ComprimentoMetros
);
resultado.AreaMetrosQuadrados =
resultado
.Corredores
.Sum(
x =>
x.AreaMetrosQuadrados
);
resultado.LarguraMediaMetros =
resultado.Corredores.Count > 0
? resultado
.Corredores
.Average(
x =>
x.LarguraMetros
)
: 0;
resultado.TempoEstimadoSegundos =
CalcularTempoEstimado(
resultado.DistanciaMetros,
velocidadeSemErvasMs,
velocidadeComErvasMs,
resultado
.PercentualErvasConsiderado
);
return resultado;
}
private void GarantirCacheDocumento(
MapaDocumento documento)
{
if (
ReferenceEquals(
_documentoCache,
documento
)
)
{
return;
}
_documentoCache =
documento;
_cacheCorredores.Clear();
}
private MapaEstimativaCorredor
ObterCorredor(
MapaDocumento documento,
MapaRua ruaA,
MapaRua ruaB)
{
string chave =
CriarChave(
ruaA.Id,
ruaB.Id
);
if (
_cacheCorredores
.TryGetValue(
chave,
out var existente
)
)
{
return existente;
}
var calculado =
CalcularCorredor(
ruaA,
ruaB
);
_cacheCorredores[
chave
] =
calculado;
return calculado;
}
private static string CriarChave(
string idA,
string idB)
{
return string.Compare(
idA,
idB,
StringComparison.OrdinalIgnoreCase
) <= 0
? $"{idA}\u001F{idB}"
: $"{idB}\u001F{idA}";
}
private static MapaEstimativaCorredor
CalcularCorredor(
MapaRua ruaA,
MapaRua ruaB)
{
if (
ruaA?.Geometria == null ||
ruaB?.Geometria == null ||
ruaA.Geometria.IsEmpty ||
ruaB.Geometria.IsEmpty ||
ruaA.Geometria.NumPoints < 2 ||
ruaB.Geometria.NumPoints < 2
)
{
throw new InvalidOperationException(
"Uma das ruas possui geometria inválida."
);
}
var metricas =
ProjetarParParaMetros(
ruaA.Geometria,
ruaB.Geometria
);
double comprimentoA =
ValorComprimentoValido(
ruaA.ComprimentoMetros
)
? ruaA.ComprimentoMetros
: metricas.A.Length;
double comprimentoB =
ValorComprimentoValido(
ruaB.ComprimentoMetros
)
? ruaB.ComprimentoMetros
: metricas.B.Length;
/*
* Regra operacional definida:
*
* corredor usa o comprimento da MAIOR rua,
* garantindo que a estimativa cubra completamente
* as duas laterais.
*/
double comprimentoCorredor =
Math.Max(
comprimentoA,
comprimentoB
);
double largura =
CalcularLarguraMedia(
metricas.A,
metricas.B
);
if (
!double.IsFinite(largura) ||
largura <= 0
)
{
throw new InvalidOperationException(
$"Não foi possível calcular a largura " +
$"entre as ruas {ruaA.Id} e {ruaB.Id}."
);
}
return new MapaEstimativaCorredor
{
RuaAId =
ruaA.Id,
RuaBId =
ruaB.Id,
ComprimentoMetros =
comprimentoCorredor,
LarguraMetros =
largura
};
}
private static bool ValorComprimentoValido(
double valor)
{
return
double.IsFinite(valor) &&
valor > 0;
}
/*
* O documento permanece WGS84.
*
* Criamos uma projeção cartesiana LOCAL apenas
* para medições métricas.
*
* Para a escala de um talhão agrícola,
* a aproximação é mais do que suficiente.
*/
private static (
LineString A,
LineString B)
ProjetarParParaMetros(
LineString linhaA,
LineString linhaB)
{
var envelope =
new Envelope(
linhaA.EnvelopeInternal
);
envelope.ExpandToInclude(
linhaB.EnvelopeInternal
);
double lonReferencia =
(
envelope.MinX +
envelope.MaxX
) /
2.0;
double latReferencia =
(
envelope.MinY +
envelope.MaxY
) /
2.0;
double latRad =
latReferencia *
Math.PI /
180.0;
const double metrosPorGrauLat =
110540.0;
double metrosPorGrauLon =
111320.0 *
Math.Cos(
latRad
);
var factory =
new GeometryFactory(
new PrecisionModel(),
0
);
LineString Converter(
LineString origem)
{
var coords =
origem
.Coordinates
.Select(
c =>
new Coordinate(
(
c.X -
lonReferencia
) *
metrosPorGrauLon,
(
c.Y -
latReferencia
) *
metrosPorGrauLat
)
)
.ToArray();
return factory
.CreateLineString(
coords
);
}
return (
Converter(linhaA),
Converter(linhaB)
);
}
private static double CalcularLarguraMedia(
LineString linhaA,
LineString linhaB)
{
var distancias =
new List<double>();
AdicionarAmostras(
linhaA,
linhaB,
distancias
);
/*
* Fazemos também B -> A.
*
* Assim a largura não muda simplesmente
* porque o operador selecionou as ruas
* na ordem inversa.
*/
AdicionarAmostras(
linhaB,
linhaA,
distancias
);
distancias =
distancias
.Where(
x =>
double.IsFinite(x) &&
x > 0
)
.OrderBy(
x =>
x
)
.ToList();
if (distancias.Count == 0)
return 0;
/*
* Média aparada de 10%.
*
* Pontinhas divergentes de rua, erros locais
* e finais desencontrados não devem distorcer
* a largura inteira do corredor.
*/
int remover =
distancias.Count >= 10
? (int)Math.Floor(
distancias.Count *
0.10
)
: 0;
int quantidade =
distancias.Count -
remover * 2;
if (quantidade <= 0)
{
return distancias.Average();
}
return distancias
.Skip(remover)
.Take(quantidade)
.Average();
}
private static void AdicionarAmostras(
LineString origem,
LineString destino,
List<double> resultado)
{
if (
origem == null ||
destino == null ||
origem.Length <= 0 ||
destino.Length <= 0
)
{
return;
}
var indexed =
new LengthIndexedLine(
origem
);
double inicio =
indexed.StartIndex;
double fim =
indexed.EndIndex;
for (
int i = 0;
i < QuantidadeAmostrasLargura;
i++)
{
/*
* 5% -> 95%.
*
* Evita que diferenças apenas nas pontas
* sejam interpretadas como largura lateral.
*/
double t =
0.05 +
(
0.90 *
i /
(
QuantidadeAmostrasLargura -
1.0
)
);
double indice =
inicio +
(
fim -
inicio
) *
t;
Coordinate coordenada =
indexed.ExtractPoint(
indice
);
var ponto =
origem.Factory
.CreatePoint(
coordenada
);
double distancia =
ponto.Distance(
destino
);
if (
double.IsFinite(
distancia
)
)
{
resultado.Add(
distancia
);
}
}
}
private static double?
CalcularTempoEstimado(
double distanciaMetros,
double? velocidadeSemErvasMs,
double? velocidadeComErvasMs,
double percentualErvas)
{
if (
!double.IsFinite(
distanciaMetros
) ||
distanciaMetros <= 0
)
{
return null;
}
double fatorErvas =
Math.Clamp(
percentualErvas,
0,
100
) /
100.0;
double fatorSemErvas =
1.0 -
fatorErvas;
if (
fatorSemErvas > 0 &&
(
!velocidadeSemErvasMs.HasValue ||
!double.IsFinite(
velocidadeSemErvasMs.Value
) ||
velocidadeSemErvasMs.Value <= 0
)
)
{
return null;
}
if (
fatorErvas > 0 &&
(
!velocidadeComErvasMs.HasValue ||
!double.IsFinite(
velocidadeComErvasMs.Value
) ||
velocidadeComErvasMs.Value <= 0
)
)
{
return null;
}
double tempo =
0;
if (fatorSemErvas > 0)
{
tempo +=
(
distanciaMetros *
fatorSemErvas
) /
velocidadeSemErvasMs!.Value;
}
if (fatorErvas > 0)
{
tempo +=
(
distanciaMetros *
fatorErvas
) /
velocidadeComErvasMs!.Value;
}
return
double.IsFinite(tempo)
? tempo
: null;
}
}
}