752 lines
18 KiB
C#
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;
|
|
}
|
|
}
|
|
} |