agrobot_base/AgroBase/OperationControl/Mapas/Editing/MapaEdicaoService.cs

1179 lines
27 KiB
C#

using NetTopologySuite.Geometries;
using OperationControl.Mapas.Models;
using OperationControl.Mapas.Services;
namespace OperationControl.Mapas.Editing
{
public sealed class MapaEdicaoService
{
private const int LimiteHistorico = 50;
private readonly MapaDocumentoCloneService _cloneService = new();
private readonly MapaRecorteService _recorteService = new();
public void Iniciar(MapaEstadoEdicao estado, MapaDocumento documento)
{
if (estado == null)
{
throw new ArgumentNullException(
nameof(estado)
);
}
if (documento == null)
{
throw new ArgumentNullException(
nameof(documento)
);
}
estado.DocumentoOriginal = _cloneService.Clonar(documento);
estado.DocumentoTrabalho = _cloneService.Clonar(documento);
estado.RuasSelecionadasMutaveis.Clear();
estado.DocumentoTrabalho.Modificado = false;
estado.HistoricoDesfazerMutavel.Clear();
estado.HistoricoRefazerMutavel.Clear();
}
public void Cancelar(MapaEstadoEdicao estado)
{
ValidarEstado(estado);
estado.DocumentoOriginal = null;
estado.DocumentoTrabalho = null;
estado.RuasSelecionadasMutaveis.Clear();
estado.HistoricoDesfazerMutavel.Clear();
estado.HistoricoRefazerMutavel.Clear();
}
public bool AlternarRua(MapaEstadoEdicao estado, string ruaId)
{
var id =
ObterIdCanonico(
estado,
ruaId
);
if (
estado
.RuasSelecionadasMutaveis
.Contains(
id
)
)
{
estado
.RuasSelecionadasMutaveis
.Remove(
id
);
return false;
}
estado
.RuasSelecionadasMutaveis
.Add(
id
);
return true;
}
public void SelecionarRua(
MapaEstadoEdicao estado,
string ruaId)
{
string id =
ObterIdCanonico(
estado,
ruaId
);
estado
.RuasSelecionadasMutaveis
.Add(
id
);
}
public void RemoverRuaSelecao(
MapaEstadoEdicao estado,
string ruaId)
{
ValidarEstadoAtivo(
estado
);
if (
string.IsNullOrWhiteSpace(
ruaId
)
)
{
return;
}
estado
.RuasSelecionadasMutaveis
.Remove(
ruaId.Trim()
);
}
public void LimparSelecao(
MapaEstadoEdicao estado)
{
ValidarEstadoAtivo(
estado
);
estado
.RuasSelecionadasMutaveis
.Clear();
}
/*
* Operação destrutiva de exclusão completa
* de ruas selecionadas.
*
* A alteração é registrada no histórico
* como uma única transação de Undo/Redo.
*/
public int RemoverRuasSelecionadas(MapaEstadoEdicao estado)
{
ValidarEstadoAtivo(
estado
);
if (
estado
.RuasSelecionadasMutaveis
.Count == 0
)
{
return 0;
}
var documento =
estado.DocumentoTrabalho!;
/*
* Descobrimos primeiro se realmente haverá
* alguma mutação.
*
* Não queremos criar Ctrl+Z vazio.
*/
bool possuiRuaRemovivel =
documento
.Ruas
.Any(
rua =>
estado
.RuasSelecionadasMutaveis
.Contains(
rua.Id
)
);
if (!possuiRuaRemovivel)
return 0;
/*
* Estado imediatamente ANTES da operação.
*
* A seleção também é preservada.
*/
var antes =
CriarSnapshot(
estado
);
int quantidadeAntes =
documento.Ruas.Count;
documento.Ruas.RemoveAll(
rua =>
estado
.RuasSelecionadasMutaveis
.Contains(
rua.Id
)
);
int removidas =
quantidadeAntes -
documento.Ruas.Count;
estado
.RuasSelecionadasMutaveis
.Clear();
if (removidas <= 0)
return 0;
documento.Modificado =
true;
/*
* Estado imediatamente DEPOIS.
*/
var depois =
CriarSnapshot(
estado
);
RegistrarAlteracao(
estado,
removidas == 1
? "Excluir 1 rua"
: $"Excluir {removidas} ruas",
antes,
depois
);
return removidas;
}
public MapaResultadoRecorte RecortarPorPoligono(MapaEstadoEdicao estado, IEnumerable<Coordinate> pontosPoligono, bool somenteSelecionadas = false)
{
ValidarEstadoAtivo(
estado
);
if (
somenteSelecionadas &&
!estado.PossuiSelecao
)
{
throw new InvalidOperationException(
"Nenhuma rua está selecionada " +
"para receber o recorte."
);
}
/*
* Snapshot ANTES da operação inteira.
*
* Se 80 ruas forem cortadas,
* ainda será apenas um Ctrl+Z.
*/
var antes =
CriarSnapshot(
estado
);
try
{
IReadOnlyCollection<string>?
escopo =
somenteSelecionadas
? estado
.RuasSelecionadas
.ToArray()
: null;
var resultado =
_recorteService
.RecortarPorPoligono(
estado
.DocumentoTrabalho!,
pontosPoligono,
escopo
);
if (
!resultado.HouveAlteracao
)
{
return resultado;
}
/*
* Uma rua completamente removida
* não pode continuar selecionada.
*/
estado
.RuasSelecionadasMutaveis
.ExceptWith(
resultado
.RuasRemovidasIds
);
var depois =
CriarSnapshot(
estado
);
RegistrarAlteracao(
estado,
$"Recorte por polígono " +
$"({resultado.RuasAlteradas + resultado.RuasRemovidas} ruas)",
antes,
depois
);
return resultado;
}
catch
{
/*
* Segurança transacional.
*
* Se algo inesperado acontecer depois
* de alguma alteração, o DocumentoTrabalho
* volta integralmente ao snapshot anterior.
*/
RestaurarSnapshot(
estado,
antes
);
throw;
}
}
public MapaResultadoRecorte ApararPorLinha(MapaEstadoEdicao estado, IEnumerable<Coordinate> pontosLinha, bool somenteSelecionadas = false)
{
ValidarEstadoAtivo(
estado
);
if (
somenteSelecionadas &&
!estado.PossuiSelecao
)
{
throw new InvalidOperationException(
"Nenhuma rua está selecionada " +
"para receber o recorte."
);
}
var antes =
CriarSnapshot(
estado
);
try
{
IReadOnlyCollection<string>?
escopo =
somenteSelecionadas
? estado
.RuasSelecionadas
.ToArray()
: null;
var resultado =
_recorteService
.ApararPorLinha(
estado
.DocumentoTrabalho!,
pontosLinha,
escopo
);
if (
!resultado.HouveAlteracao
)
{
return resultado;
}
var depois =
CriarSnapshot(
estado
);
RegistrarAlteracao(
estado,
$"Aparar por borda " +
$"({resultado.RuasAlteradas} ruas)",
antes,
depois
);
return resultado;
}
catch
{
RestaurarSnapshot(
estado,
antes
);
throw;
}
}
public bool RestaurarDocumentoFonte(MapaEstadoEdicao estado, MapaDocumento documentoFonte)
{
ValidarEstadoAtivo(
estado
);
if (documentoFonte == null)
{
throw new ArgumentNullException(
nameof(documentoFonte)
);
}
/*
* Não criamos histórico vazio.
*
* Se o documento de trabalho já corresponde
* exatamente à fonte, não há nada para fazer.
*/
if (
DocumentosEquivalentes(
estado.DocumentoTrabalho!,
documentoFonte
)
)
{
return false;
}
/*
* Tudo que existia imediatamente antes
* da restauração pode voltar com Ctrl+Z.
*/
MapaEdicaoSnapshot antes =
CriarSnapshot(
estado
);
/*
* Nunca entregamos DocumentoFonte diretamente.
*
* O editor recebe outra cópia profunda.
*/
estado.DocumentoTrabalho =
_cloneService
.Clonar(
documentoFonte
);
/*
* Embora geometricamente seja o documento-fonte,
* dentro desta sessão isso representa uma alteração
* em relação ao documento oficial atual.
*
* Portanto Apply precisa ficar habilitado.
*/
estado.DocumentoTrabalho.Modificado =
true;
/*
* A seleção de edição pertence ao documento
* anterior.
*
* Limpamos para evitar uma seleção cirúrgica
* antiga sendo aplicada acidentalmente depois
* da restauração.
*
* A seleção OPERACIONAL do Workspace não passa
* por aqui.
*/
estado
.RuasSelecionadasMutaveis
.Clear();
MapaEdicaoSnapshot depois =
CriarSnapshot(
estado
);
RegistrarAlteracao(
estado,
"Restaurar mapa original",
antes,
depois
);
return true;
}
/*
* Devolve uma cópia.
*
* Nunca entregamos diretamente
* DocumentoTrabalho para outra camada
* começar a mutá-lo escondido.
*/
public MapaDocumento ObterResultado(MapaEstadoEdicao estado)
{
ValidarEstadoAtivo(estado);
return _cloneService.Clonar(estado.DocumentoTrabalho!);
}
private static string ObterIdCanonico(
MapaEstadoEdicao estado,
string ruaId)
{
ValidarEstadoAtivo(
estado
);
if (
string.IsNullOrWhiteSpace(
ruaId
)
)
{
throw new ArgumentException(
"ID da rua inválido.",
nameof(ruaId)
);
}
var correspondencias =
estado
.DocumentoTrabalho!
.Ruas
.Where(
x =>
string.Equals(
x.Id,
ruaId.Trim(),
StringComparison
.OrdinalIgnoreCase
)
)
.ToList();
if (correspondencias.Count == 0)
{
throw new InvalidOperationException(
$"Rua '{ruaId}' não pertence " +
"ao documento em edição."
);
}
if (correspondencias.Count > 1)
{
throw new InvalidOperationException(
$"O ID '{ruaId}' está ambíguo " +
"no documento em edição."
);
}
return correspondencias[0].Id;
}
private MapaEdicaoSnapshot CriarSnapshot(MapaEstadoEdicao estado)
{
ValidarEstadoAtivo(estado);
return new MapaEdicaoSnapshot(
_cloneService.Clonar(estado.DocumentoTrabalho!),
estado.RuasSelecionadasMutaveis
);
}
private void RestaurarSnapshot(MapaEstadoEdicao estado, MapaEdicaoSnapshot snapshot)
{
ValidarEstadoAtivo(estado);
if (snapshot == null)
{
throw new ArgumentNullException(
nameof(snapshot)
);
}
estado.DocumentoTrabalho = _cloneService.Clonar(snapshot.Documento);
estado.RuasSelecionadasMutaveis.Clear();
var idsValidos =
estado
.DocumentoTrabalho
.Ruas
.Select(x => x.Id)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
foreach (string ruaId in snapshot.RuasSelecionadas)
{
if (idsValidos.Contains(ruaId))
{
estado.RuasSelecionadasMutaveis.Add(ruaId);
}
}
}
private static void RegistrarAlteracao(MapaEstadoEdicao estado, string descricao, MapaEdicaoSnapshot antes, MapaEdicaoSnapshot depois)
{
var item =
new MapaEdicaoHistoricoItem(
descricao,
antes,
depois
);
estado
.HistoricoDesfazerMutavel
.Add(
item
);
/*
* Uma edição nova depois de um Undo
* abre uma nova linha temporal.
*
* O Redo anterior deixa de fazer sentido.
*/
estado
.HistoricoRefazerMutavel
.Clear();
while (
estado
.HistoricoDesfazerMutavel
.Count >
LimiteHistorico
)
{
estado
.HistoricoDesfazerMutavel
.RemoveAt(
0
);
}
}
public bool PodeDesfazer(MapaEstadoEdicao estado)
{
ValidarEstado(
estado
);
return
estado.EmEdicao &&
estado.PodeDesfazer;
}
public bool PodeRefazer(MapaEstadoEdicao estado)
{
ValidarEstado(
estado
);
return
estado.EmEdicao &&
estado.PodeRefazer;
}
public bool Desfazer(MapaEstadoEdicao estado)
{
ValidarEstadoAtivo(
estado
);
if (!estado.PodeDesfazer)
return false;
int indice =
estado
.HistoricoDesfazerMutavel
.Count -
1;
var item =
estado
.HistoricoDesfazerMutavel[
indice
];
estado
.HistoricoDesfazerMutavel
.RemoveAt(
indice
);
RestaurarSnapshot(
estado,
item.Antes
);
estado
.HistoricoRefazerMutavel
.Add(
item
);
return true;
}
public bool Refazer(MapaEstadoEdicao estado)
{
ValidarEstadoAtivo(
estado
);
if (!estado.PodeRefazer)
return false;
int indice =
estado
.HistoricoRefazerMutavel
.Count -
1;
var item =
estado
.HistoricoRefazerMutavel[
indice
];
estado
.HistoricoRefazerMutavel
.RemoveAt(
indice
);
RestaurarSnapshot(
estado,
item.Depois
);
estado
.HistoricoDesfazerMutavel
.Add(
item
);
return true;
}
private static void ValidarEstado(
MapaEstadoEdicao estado)
{
if (estado == null)
{
throw new ArgumentNullException(
nameof(estado)
);
}
}
private static void ValidarEstadoAtivo(
MapaEstadoEdicao estado)
{
ValidarEstado(
estado
);
if (
estado.DocumentoOriginal == null ||
estado.DocumentoTrabalho == null
)
{
throw new InvalidOperationException(
"Não existe uma sessão de edição ativa."
);
}
}
public IReadOnlyList<string> SelecionarPorRetangulo(
MapaEstadoEdicao estado,
double minLatitude,
double minLongitude,
double maxLatitude,
double maxLongitude,
bool substituirSelecao = true)
{
if (estado == null)
{
throw new ArgumentNullException(
nameof(estado)
);
}
if (
!estado.EmEdicao ||
estado.DocumentoTrabalho == null
)
{
throw new InvalidOperationException(
"Não existe uma sessão de edição ativa."
);
}
double minLat =
Math.Min(
minLatitude,
maxLatitude
);
double maxLat =
Math.Max(
minLatitude,
maxLatitude
);
double minLon =
Math.Min(
minLongitude,
maxLongitude
);
double maxLon =
Math.Max(
minLongitude,
maxLongitude
);
if (
minLat < -90 ||
maxLat > 90 ||
minLon < -180 ||
maxLon > 180
)
{
throw new ArgumentOutOfRangeException(
"Retângulo WGS84 inválido."
);
}
var envelope =
new Envelope(
minLon,
maxLon,
minLat,
maxLat
);
/*
* Documento canônico é WGS84.
*/
var geometryFactory =
new GeometryFactory(
new PrecisionModel(),
4326
);
Geometry area =
geometryFactory
.ToGeometry(
envelope
);
var encontrados =
new List<string>();
foreach (
var rua
in estado
.DocumentoTrabalho
.Ruas)
{
if (
rua == null ||
rua.Geometria == null ||
rua.Geometria.IsEmpty
)
{
continue;
}
/*
* Filtro barato primeiro.
*/
if (
!rua.Geometria
.EnvelopeInternal
.Intersects(
envelope
)
)
{
continue;
}
/*
* Teste real.
*
* Não basta o bounding box da rua
* tocar o retângulo.
*/
if (
!rua.Geometria
.Intersects(
area
)
)
{
continue;
}
encontrados.Add(
rua.Id
);
}
if (substituirSelecao)
{
LimparSelecao(estado);
}
foreach (string ruaId in encontrados)
{
SelecionarRua(estado, ruaId);
}
return encontrados;
}
private static bool DocumentosEquivalentes(MapaDocumento atual, MapaDocumento fonte)
{
if (
atual == null ||
fonte == null
)
{
return false;
}
/*
* Modificado NÃO entra nessa comparação.
*
* Queremos saber se o conteúdo canônico
* é equivalente, não o estado da sessão.
*/
if (
!string.Equals(
atual.Nome,
fonte.Nome,
StringComparison.Ordinal
) ||
!string.Equals(
atual.CaminhoOrigem,
fonte.CaminhoOrigem,
StringComparison.Ordinal
) ||
atual.FormatoOrigem !=
fonte.FormatoOrigem ||
atual.Tipo !=
fonte.Tipo
)
{
return false;
}
if (
atual.Ruas.Count !=
fonte.Ruas.Count
)
{
return false;
}
for (
int i = 0;
i < atual.Ruas.Count;
i++)
{
MapaRua a =
atual.Ruas[i];
MapaRua b =
fonte.Ruas[i];
if (
!string.Equals(
a.Id,
b.Id,
StringComparison.Ordinal
) ||
!string.Equals(
a.IdOriginal,
b.IdOriginal,
StringComparison.Ordinal
) ||
a.IdGeradoAutomaticamente !=
b.IdGeradoAutomaticamente ||
!string.Equals(
a.Nome,
b.Nome,
StringComparison.Ordinal
) ||
Math.Abs(
a.ComprimentoMetros -
b.ComprimentoMetros
) > 0.000001 ||
Math.Abs(
a.Dist1 -
b.Dist1
) > 0.000001 ||
Math.Abs(
a.Dist2 -
b.Dist2
) > 0.000001 ||
a.IndiceFeatureOrigem !=
b.IndiceFeatureOrigem
)
{
return false;
}
bool geometriaANula =
a.Geometria == null;
bool geometriaBNula =
b.Geometria == null;
if (
geometriaANula !=
geometriaBNula
)
{
return false;
}
if (
!geometriaANula &&
!a.Geometria.EqualsExact(
b.Geometria
)
)
{
return false;
}
}
return true;
}
}
}