agrobot_base/AgroBase/OperationControl/Mapas/Services/MapaRecorteService.cs

1617 lines
38 KiB
C#

using NetTopologySuite.Geometries;
using NetTopologySuite.LinearReferencing;
using OperationControl.Mapas.Editing;
using OperationControl.Mapas.Models;
using Point = NetTopologySuite.Geometries.Point;
namespace OperationControl.Mapas.Services
{
/// <summary>
/// Núcleo geométrico das operações de recorte.
///
/// Não conhece:
/// - Mapsui;
/// - WPF;
/// - GNSS;
/// - Controller;
/// - Undo/Redo.
///
/// Recebe geometrias WGS84 e altera somente
/// o MapaDocumento informado.
/// </summary>
public sealed class MapaRecorteService
{
private const double
ToleranciaFragmentoMetros =
0.05;
private const double
ToleranciaIntersecaoMetros =
0.02;
private readonly GeometryFactory
_geometryFactory =
new(
new PrecisionModel(),
4326
);
/*
* =========================================================
* POLÍGONO
* =========================================================
*/
public MapaResultadoRecorte
RecortarPorPoligono(
MapaDocumento documento,
IEnumerable<Coordinate>
pontosPoligono,
IReadOnlyCollection<string>?
ruasPermitidas = null)
{
ValidarDocumento(
documento
);
Polygon poligono =
CriarPoligono(
pontosPoligono
);
HashSet<string>?
filtroIds =
CriarFiltroIds(
ruasPermitidas
);
var resultado =
new MapaResultadoRecorte();
var alteracoes =
new List<AlteracaoRua>();
foreach (
var rua
in documento.Ruas.ToArray())
{
if (
rua == null ||
rua.Geometria == null ||
rua.Geometria.IsEmpty ||
rua.Geometria.NumPoints < 2
)
{
continue;
}
if (
filtroIds != null &&
!filtroIds.Contains(
rua.Id
)
)
{
continue;
}
resultado.RuasAnalisadas++;
/*
* Filtro barato primeiro.
*/
if (
!rua.Geometria
.EnvelopeInternal
.Intersects(
poligono
.EnvelopeInternal
)
)
{
resultado
.RuasIgnoradas++;
continue;
}
try
{
if (
!rua.Geometria
.Intersects(
poligono
)
)
{
resultado
.RuasIgnoradas++;
continue;
}
double comprimentoAntes =
CalcularComprimentoMetros(
rua.Geometria
);
Geometry restante =
rua.Geometria
.Difference(
poligono
);
/*
* Rua totalmente coberta pelo
* polígono.
*/
if (
restante == null ||
restante.IsEmpty
)
{
alteracoes.Add(
AlteracaoRua.Remover(
rua,
comprimentoAntes
)
);
resultado
.RuasRemovidasIds
.Add(
rua.Id
);
resultado
.ComprimentoRemovidoMetros +=
comprimentoAntes;
continue;
}
List<LineString>
fragmentos =
ExtrairLineStrings(
restante
);
/*
* Nenhum segmento linear restou.
*/
if (
fragmentos.Count == 0
)
{
alteracoes.Add(
AlteracaoRua.Remover(
rua,
comprimentoAntes
)
);
resultado
.RuasRemovidasIds
.Add(
rua.Id
);
resultado
.ComprimentoRemovidoMetros +=
comprimentoAntes;
continue;
}
/*
* O polígono removeu uma parte
* central da rua.
*
* Exemplo:
*
* ----XXXXXXXX----
*
* Isso exigiria criar duas ruas.
*
* Nesta V1 deixamos intacta
* e marcamos como ambígua.
*/
if (
fragmentos.Count != 1
)
{
resultado
.RuasAmbiguasIds
.Add(
rua.Id
);
continue;
}
LineString novoTrecho =
NormalizarOrientacao(
fragmentos[0],
rua.Geometria
);
double comprimentoDepois =
CalcularComprimentoMetros(
novoTrecho
);
double removido =
comprimentoAntes -
comprimentoDepois;
/*
* Pode acontecer quando o polígono
* apenas encosta na rua.
*/
if (
removido <=
ToleranciaFragmentoMetros
)
{
resultado
.RuasIgnoradas++;
continue;
}
/*
* Restou praticamente zero.
*/
if (
comprimentoDepois <=
ToleranciaFragmentoMetros
)
{
alteracoes.Add(
AlteracaoRua.Remover(
rua,
comprimentoAntes
)
);
resultado
.RuasRemovidasIds
.Add(
rua.Id
);
resultado
.ComprimentoRemovidoMetros +=
comprimentoAntes;
continue;
}
alteracoes.Add(
AlteracaoRua.Substituir(
rua,
novoTrecho,
comprimentoDepois
)
);
resultado
.RuasAlteradasIds
.Add(
rua.Id
);
resultado
.ComprimentoRemovidoMetros +=
removido;
}
catch (
TopologyException
)
{
/*
* Nunca fazemos uma decisão destrutiva
* quando a topologia é duvidosa.
*/
resultado
.RuasAmbiguasIds
.Add(
rua.Id
);
}
}
AplicarAlteracoes(
documento,
alteracoes
);
return resultado;
}
/*
* =========================================================
* LINHA DE BORDA
* =========================================================
*/
public MapaResultadoRecorte
ApararPorLinha(
MapaDocumento documento,
IEnumerable<Coordinate>
pontosLinha,
IReadOnlyCollection<string>?
ruasPermitidas = null)
{
ValidarDocumento(
documento
);
LineString linhaCorte =
CriarLinha(
pontosLinha
);
HashSet<string>?
filtroIds =
CriarFiltroIds(
ruasPermitidas
);
var resultado =
new MapaResultadoRecorte();
var alteracoes =
new List<AlteracaoRua>();
foreach (
var rua
in documento.Ruas.ToArray())
{
if (
rua == null ||
rua.Geometria == null ||
rua.Geometria.IsEmpty ||
rua.Geometria.NumPoints < 2
)
{
continue;
}
if (
filtroIds != null &&
!filtroIds.Contains(
rua.Id
)
)
{
continue;
}
resultado.RuasAnalisadas++;
if (
!rua.Geometria
.EnvelopeInternal
.Intersects(
linhaCorte
.EnvelopeInternal
)
)
{
resultado
.RuasIgnoradas++;
continue;
}
try
{
Geometry intersecao =
rua.Geometria
.Intersection(
linhaCorte
);
if (
intersecao == null ||
intersecao.IsEmpty
)
{
resultado
.RuasIgnoradas++;
continue;
}
bool possuiTrechoSobreposto =
false;
List<Coordinate>
pontosIntersecao =
ExtrairPontosIntersecao(
intersecao,
ref possuiTrechoSobreposto
);
/*
* Se a linha GNSS andar exatamente
* sobre uma rua, não existe uma
* interpretação segura.
*/
if (
possuiTrechoSobreposto
)
{
resultado
.RuasAmbiguasIds
.Add(
rua.Id
);
continue;
}
/*
* Exigimos exatamente UM cruzamento
* da borda com a rua.
*/
if (
pontosIntersecao.Count != 1
)
{
if (
pontosIntersecao.Count == 0
)
{
resultado
.RuasIgnoradas++;
}
else
{
resultado
.RuasAmbiguasIds
.Add(
rua.Id
);
}
continue;
}
var indexed =
new LengthIndexedLine(
rua.Geometria
);
double indice =
indexed.Project(
pontosIntersecao[0]
);
Geometry trechoA =
indexed.ExtractLine(
indexed.StartIndex,
indice
);
Geometry trechoB =
indexed.ExtractLine(
indice,
indexed.EndIndex
);
var fragmentosA =
ExtrairLineStrings(
trechoA
);
var fragmentosB =
ExtrairLineStrings(
trechoB
);
if (
fragmentosA.Count != 1 ||
fragmentosB.Count != 1
)
{
resultado
.RuasAmbiguasIds
.Add(
rua.Id
);
continue;
}
LineString linhaA =
NormalizarOrientacao(
fragmentosA[0],
rua.Geometria
);
LineString linhaB =
NormalizarOrientacao(
fragmentosB[0],
rua.Geometria
);
double comprimentoA =
CalcularComprimentoMetros(
linhaA
);
double comprimentoB =
CalcularComprimentoMetros(
linhaB
);
double menor =
Math.Min(
comprimentoA,
comprimentoB
);
/*
* Cruzamento praticamente no extremo.
* Não existe recorte material.
*/
if (
menor <=
ToleranciaFragmentoMetros
)
{
resultado
.RuasIgnoradas++;
continue;
}
/*
* Se os dois lados forem praticamente
* iguais, não existe um "menor lado"
* confiável.
*/
if (
Math.Abs(
comprimentoA -
comprimentoB
) <=
ToleranciaFragmentoMetros
)
{
resultado
.RuasAmbiguasIds
.Add(
rua.Id
);
continue;
}
LineString manter;
double comprimentoManter;
if (
comprimentoA >
comprimentoB
)
{
manter =
linhaA;
comprimentoManter =
comprimentoA;
}
else
{
manter =
linhaB;
comprimentoManter =
comprimentoB;
}
manter =
NormalizarOrientacao(
manter,
rua.Geometria
);
alteracoes.Add(
AlteracaoRua.Substituir(
rua,
manter,
comprimentoManter
)
);
resultado
.RuasAlteradasIds
.Add(
rua.Id
);
resultado
.ComprimentoRemovidoMetros +=
menor;
}
catch (
TopologyException
)
{
resultado
.RuasAmbiguasIds
.Add(
rua.Id
);
}
}
AplicarAlteracoes(
documento,
alteracoes
);
return resultado;
}
/*
* =========================================================
* CONSTRUÇÃO DAS GEOMETRIAS DE ENTRADA
* =========================================================
*/
private Polygon CriarPoligono(
IEnumerable<Coordinate>
pontos)
{
List<Coordinate>
normalizados =
NormalizarPontos(
pontos,
3
);
Coordinate primeiro =
normalizados[0];
Coordinate ultimo =
normalizados[
normalizados.Count -
1
];
if (
DistanciaMetros(
primeiro,
ultimo
) <=
0.001
)
{
normalizados[
normalizados.Count -
1
] =
CopiarCoordinate(
primeiro
);
}
else
{
normalizados.Add(
CopiarCoordinate(
primeiro
)
);
}
if (
normalizados.Count < 4
)
{
throw new InvalidOperationException(
"O polígono de recorte " +
"não possui vértices suficientes."
);
}
var ring =
_geometryFactory
.CreateLinearRing(
normalizados
.ToArray()
);
var poligono =
_geometryFactory
.CreatePolygon(
ring
);
if (
poligono.IsEmpty ||
!poligono.IsValid ||
poligono.Area <= 0
)
{
throw new InvalidOperationException(
"O polígono de recorte " +
"é inválido ou se auto-intersecta."
);
}
return poligono;
}
private LineString CriarLinha(
IEnumerable<Coordinate>
pontos)
{
List<Coordinate>
normalizados =
NormalizarPontos(
pontos,
2
);
var linha =
_geometryFactory
.CreateLineString(
normalizados
.ToArray()
);
if (
linha.IsEmpty ||
linha.NumPoints < 2
)
{
throw new InvalidOperationException(
"A linha de borda é inválida."
);
}
return linha;
}
private static List<Coordinate>
NormalizarPontos(
IEnumerable<Coordinate>
pontos,
int minimo)
{
if (pontos == null)
{
throw new ArgumentNullException(
nameof(pontos)
);
}
var resultado =
new List<Coordinate>();
foreach (
Coordinate? ponto
in pontos)
{
if (ponto == null)
continue;
ValidarCoordenada(
ponto
);
var copia =
CopiarCoordinate(
ponto
);
if (
resultado.Count > 0 &&
DistanciaMetros(
resultado[
resultado.Count -
1
],
copia
) <=
0.001
)
{
continue;
}
resultado.Add(
copia
);
}
if (
resultado.Count <
minimo
)
{
throw new InvalidOperationException(
$"São necessários pelo menos " +
$"{minimo} pontos distintos."
);
}
return resultado;
}
/*
* =========================================================
* EXTRAÇÃO DE RESULTADOS NTS
* =========================================================
*/
private static List<LineString>
ExtrairLineStrings(
Geometry geometria)
{
var resultado =
new List<LineString>();
ExtrairLineStringsRecursivo(
geometria,
resultado
);
return resultado
.Where(
x =>
x != null &&
!x.IsEmpty &&
x.NumPoints >= 2 &&
CalcularComprimentoMetros(
x
) >
0.001
)
.ToList();
}
private static void
ExtrairLineStringsRecursivo(
Geometry geometria,
List<LineString> resultado)
{
if (
geometria == null ||
geometria.IsEmpty
)
{
return;
}
if (
geometria
is LineString linha
)
{
resultado.Add(
linha
);
return;
}
if (
geometria
is GeometryCollection colecao
)
{
for (
int i = 0;
i <
colecao.NumGeometries;
i++)
{
ExtrairLineStringsRecursivo(
colecao
.GetGeometryN(
i
),
resultado
);
}
}
}
private static List<Coordinate>
ExtrairPontosIntersecao(
Geometry geometria,
ref bool possuiTrechoLinear)
{
var resultado =
new List<Coordinate>();
ExtrairPontosIntersecaoRecursivo(
geometria,
resultado,
ref possuiTrechoLinear
);
/*
* Elimina duplicatas que podem surgir
* quando o cruzamento ocorre exatamente
* em um vértice.
*/
var unicos =
new List<Coordinate>();
foreach (
var ponto
in resultado)
{
bool repetido =
unicos.Any(
existente =>
DistanciaMetros(
existente,
ponto
) <=
ToleranciaIntersecaoMetros
);
if (!repetido)
{
unicos.Add(
ponto
);
}
}
return unicos;
}
private static void
ExtrairPontosIntersecaoRecursivo(
Geometry geometria,
List<Coordinate> resultado,
ref bool possuiTrechoLinear)
{
if (
geometria == null ||
geometria.IsEmpty
)
{
return;
}
if (
geometria
is Point ponto)
{
resultado.Add(
CopiarCoordinate(
ponto.Coordinate
)
);
return;
}
if (
geometria
is LineString linha)
{
if (
CalcularComprimentoMetros(
linha
) >
ToleranciaIntersecaoMetros
)
{
possuiTrechoLinear =
true;
}
return;
}
if (
geometria
is GeometryCollection colecao)
{
for (
int i = 0;
i <
colecao.NumGeometries;
i++)
{
ExtrairPontosIntersecaoRecursivo(
colecao
.GetGeometryN(
i
),
resultado,
ref possuiTrechoLinear
);
}
}
}
/*
* =========================================================
* COMMIT DAS ALTERAÇÕES
* =========================================================
*/
private static void AplicarAlteracoes(
MapaDocumento documento,
IEnumerable<AlteracaoRua>
alteracoes)
{
bool alterou =
false;
foreach (
var alteracao
in alteracoes)
{
if (
alteracao.RemoverRua
)
{
documento
.Ruas
.Remove(
alteracao.Rua
);
alterou =
true;
continue;
}
if (
alteracao.NovaGeometria
== null
)
{
continue;
}
alteracao
.Rua
.Geometria =
alteracao
.NovaGeometria;
alteracao
.Rua
.ComprimentoMetros =
alteracao
.NovoComprimentoMetros;
alterou =
true;
}
if (alterou)
{
documento.Modificado =
true;
}
}
/*
* =========================================================
* ORIENTAÇÃO
* =========================================================
*/
private static LineString
NormalizarOrientacao(
LineString fragmento,
LineString original)
{
Coordinate inicioOriginal =
original
.Coordinates
.First();
Coordinate inicioFragmento =
fragmento
.Coordinates
.First();
Coordinate fimFragmento =
fragmento
.Coordinates
.Last();
double distanciaInicio =
DistanciaMetros(
inicioOriginal,
inicioFragmento
);
double distanciaFim =
DistanciaMetros(
inicioOriginal,
fimFragmento
);
Coordinate[] coords =
fragmento
.Coordinates
.Select(
CopiarCoordinate
)
.ToArray();
if (
distanciaFim <
distanciaInicio
)
{
Array.Reverse(
coords
);
}
var resultado =
original
.Factory
.CreateLineString(
coords
);
resultado.SRID =
original.SRID;
resultado.UserData =
original.UserData;
return resultado;
}
/*
* =========================================================
* DISTÂNCIA WGS84
* =========================================================
*/
public static double
CalcularComprimentoMetros(
LineString linha)
{
if (
linha == null ||
linha.IsEmpty ||
linha.NumPoints < 2
)
{
return 0;
}
Coordinate[] coords =
linha.Coordinates;
double total =
0;
for (
int i = 1;
i < coords.Length;
i++)
{
total +=
DistanciaMetros(
coords[i - 1],
coords[i]
);
}
return total;
}
private static double DistanciaMetros(
Coordinate a,
Coordinate b)
{
const double raioTerra =
6371008.8;
double lat1 =
GrausParaRadianos(
a.Y
);
double lat2 =
GrausParaRadianos(
b.Y
);
double deltaLat =
GrausParaRadianos(
b.Y -
a.Y
);
double deltaLon =
GrausParaRadianos(
b.X -
a.X
);
double senLat =
Math.Sin(
deltaLat /
2.0
);
double senLon =
Math.Sin(
deltaLon /
2.0
);
double h =
senLat *
senLat +
Math.Cos(
lat1
) *
Math.Cos(
lat2
) *
senLon *
senLon;
h =
Math.Clamp(
h,
0,
1
);
double c =
2.0 *
Math.Asin(
Math.Sqrt(
h
)
);
return
raioTerra *
c;
}
private static double
GrausParaRadianos(
double graus)
{
return
graus *
Math.PI /
180.0;
}
/*
* =========================================================
* VALIDAÇÃO
* =========================================================
*/
private static void ValidarDocumento(
MapaDocumento documento)
{
if (documento == null)
{
throw new ArgumentNullException(
nameof(documento)
);
}
}
private static void ValidarCoordenada(
Coordinate ponto)
{
if (
!double.IsFinite(
ponto.X
) ||
!double.IsFinite(
ponto.Y
) ||
ponto.X < -180 ||
ponto.X > 180 ||
ponto.Y < -90 ||
ponto.Y > 90
)
{
throw new ArgumentOutOfRangeException(
nameof(ponto),
"Coordenada WGS84 inválida."
);
}
}
private static Coordinate
CopiarCoordinate(
Coordinate origem)
{
return new Coordinate(
origem.X,
origem.Y
);
}
private static HashSet<string>?
CriarFiltroIds(
IReadOnlyCollection<string>?
ruasPermitidas)
{
if (
ruasPermitidas == null
)
{
/*
* null = todas as ruas
*/
return null;
}
return new HashSet<string>(
ruasPermitidas
.Where(
x =>
!string.IsNullOrWhiteSpace(
x
)
),
StringComparer.OrdinalIgnoreCase
);
}
/*
* =========================================================
* MODELO INTERNO DE ALTERAÇÃO
* =========================================================
*/
private sealed class AlteracaoRua
{
public MapaRua Rua
{
get;
}
public bool RemoverRua
{
get;
}
public LineString?
NovaGeometria
{
get;
}
public double
NovoComprimentoMetros
{
get;
}
private AlteracaoRua(
MapaRua rua,
bool removerRua,
LineString?
novaGeometria,
double
novoComprimentoMetros)
{
Rua =
rua;
RemoverRua =
removerRua;
NovaGeometria =
novaGeometria;
NovoComprimentoMetros =
novoComprimentoMetros;
}
public static AlteracaoRua
Remover(
MapaRua rua,
double comprimentoAnterior)
{
return new AlteracaoRua(
rua,
true,
null,
0
);
}
public static AlteracaoRua
Substituir(
MapaRua rua,
LineString geometria,
double comprimento)
{
return new AlteracaoRua(
rua,
false,
geometria,
comprimento
);
}
}
}
}