This commit is contained in:
Diego Freitas 2026-09-25 15:24:36 -03:00
commit 19392738f7
15 changed files with 3805 additions and 6 deletions

View File

@ -3,6 +3,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:m="clr-namespace:Mapsui.UI.Wpf;assembly=Mapsui.UI.Wpf"
xmlns:editing="clr-namespace:OperationControl.Mapas.Editing.Views"
xmlns:creation="clr-namespace:OperationControl.Mapas.Creation.Views"
x:Name="Root"
MinHeight="100"
MinWidth="300"
@ -91,5 +92,8 @@
<!-- COCKPIT DO EDITOR DE MAPAS -->
<editing:MapaEditorOverlay x:Name="EditorOverlay" Panel.ZIndex="100"/>
<!-- COCKPIT DA CRIAÇÃO DE MAPAS -->
<creation:MapaCriacaoOverlay x:Name="CriacaoOverlay" Panel.ZIndex="110"/>
</Grid>
</UserControl>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,844 @@
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."
);
}
}
}
}

View File

@ -0,0 +1,62 @@
using static AgroBase.Models.Enums;
namespace OperationControl.Mapas.Creation
{
/// <summary>
/// Estado transitório da criação de mapa.
/// Não altera o Workspace até o operador explicitamente concluir.
/// </summary>
public sealed class MapaEstadoCriacao
{
private readonly List<MapaRuaCriacao> _ruas = new();
public bool EmCriacao { get; internal set; }
public string Nome { get; internal set; } = string.Empty;
public TipoMapaOperacao Tipo { get; internal set; } = TipoMapaOperacao.Indefinido;
public MapaModoCapturaCriacao ModoCaptura { get; internal set; } = MapaModoCapturaCriacao.Manual;
public bool GravacaoAutomaticaAtiva { get; internal set; }
public double DistanciaMinimaCapturaAutomaticaMetros { get; internal set; } = 0.50;
public int IndiceRuaAtual { get; internal set; }
public IReadOnlyList<MapaRuaCriacao> Ruas => _ruas;
internal List<MapaRuaCriacao> RuasMutaveis => _ruas;
public int QuantidadeRuas => _ruas.Count;
public int QuantidadeRuasValidas => _ruas.Count(x => x != null && x.Valida);
public int QuantidadePontosTotal => _ruas.Sum(x => x?.QuantidadePontos ?? 0);
public MapaRuaCriacao? RuaAtual
{
get
{
if (IndiceRuaAtual < 0 || IndiceRuaAtual >= _ruas.Count)
return null;
return _ruas[IndiceRuaAtual];
}
}
public bool PodeCriarNovaRua =>
EmCriacao &&
!GravacaoAutomaticaAtiva &&
RuaAtual?.Valida == true;
public bool PodeSalvar =>
EmCriacao &&
!GravacaoAutomaticaAtiva &&
_ruas.Count > 0 &&
_ruas.All(x => x != null && x.Valida);
internal void Limpar()
{
_ruas.Clear();
EmCriacao = false;
Nome = string.Empty;
Tipo = TipoMapaOperacao.Indefinido;
ModoCaptura = MapaModoCapturaCriacao.Manual;
GravacaoAutomaticaAtiva = false;
DistanciaMinimaCapturaAutomaticaMetros = 0.50;
IndiceRuaAtual = 0;
}
}
}

View File

@ -0,0 +1,11 @@
namespace OperationControl.Mapas.Creation
{
/// <summary>
/// Forma como os pontos da rua atual são registrados.
/// </summary>
public enum MapaModoCapturaCriacao
{
Manual = 0,
Automatico = 1
}
}

View File

@ -0,0 +1,33 @@
using static AgroBase.Models.Enums;
namespace OperationControl.Mapas.Creation
{
/// <summary>
/// Snapshot de um ponto coletado durante a criação de um mapa.
/// Latitude/Longitude são sempre WGS84.
/// </summary>
public sealed class MapaPontoCriacao
{
public double Latitude { get; set; }
public double Longitude { get; set; }
public double Altitude { get; set; }
public double PrecisaoCm { get; set; }
public int NumeroSatelites { get; set; }
public TiposCorrecaoGPS QualidadeFix { get; set; }
public DateTime Momento { get; set; }
public MapaPontoCriacao Clonar()
{
return new MapaPontoCriacao
{
Latitude = Latitude,
Longitude = Longitude,
Altitude = Altitude,
PrecisaoCm = PrecisaoCm,
NumeroSatelites = NumeroSatelites,
QualidadeFix = QualidadeFix,
Momento = Momento
};
}
}
}

View File

@ -0,0 +1,53 @@
namespace OperationControl.Mapas.Creation
{
/// <summary>
/// DTO pronto para alimentar o cockpit de criação.
///
/// O bloco GNSS é preenchido pelo Controller, pois o service
/// de domínio não conhece GpsService/GPSModel.
/// </summary>
public sealed class MapaResumoCriacao
{
public bool EmCriacao { get; init; }
public string Nome { get; init; } =
string.Empty;
public int RuaAtual { get; init; }
public int QuantidadeRuas { get; init; }
public int QuantidadeRuasValidas { get; init; }
public int PontosRuaAtual { get; init; }
public int PontosTotal { get; init; }
public double ComprimentoRuaAtualMetros { get; init; }
public double ComprimentoTotalMetros { get; init; }
public MapaModoCapturaCriacao ModoCaptura { get; init; }
public bool GravacaoAutomaticaAtiva { get; init; }
public double DistanciaMinimaAutomaticaMetros { get; init; }
public bool PodeNovaRua { get; init; }
public bool PodeSalvar { get; init; }
public IReadOnlyList<MapaRuaResumoCriacao> Ruas { get; init; } =
Array.Empty<MapaRuaResumoCriacao>();
/*
* Estado do GNSS local para o cockpit.
*/
public bool LeituraGnssValida { get; init; }
public bool QualidadeAlta { get; init; }
public string StatusGnss { get; init; } =
"GNSS sem leitura.";
}
}

View File

@ -0,0 +1,18 @@
namespace OperationControl.Mapas.Creation
{
/// <summary>
/// Uma rua ainda em levantamento.
/// O ID canônico definitivo é gerado na conversão para MapaDocumento.
/// </summary>
public sealed class MapaRuaCriacao
{
private readonly List<MapaPontoCriacao> _pontos = new();
public int Numero { get; internal set; }
public IReadOnlyList<MapaPontoCriacao> Pontos => _pontos;
public int QuantidadePontos => _pontos.Count;
public bool Valida => _pontos.Count >= 2;
internal List<MapaPontoCriacao> PontosMutaveis => _pontos;
}
}

View File

@ -0,0 +1,25 @@
namespace OperationControl.Mapas.Creation
{
/// <summary>
/// Linha resumida exibida no seletor de ruas do cockpit.
/// </summary>
public sealed class MapaRuaResumoCriacao
{
public int Numero { get; init; }
public int QuantidadePontos { get; init; }
public double ComprimentoMetros { get; init; }
public bool Valida { get; init; }
public bool Atual { get; init; }
public string Descricao =>
$"Rua {Numero} • " +
$"{QuantidadePontos} pts • " +
$"{ComprimentoMetros:F1} m" +
(Atual ? " • atual" : string.Empty);
}
}

View File

@ -0,0 +1,273 @@
<UserControl x:Class="OperationControl.Mapas.Creation.Views.MapaCriacaoOverlay"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Visibility="Collapsed"
IsHitTestVisible="False">
<UserControl.Resources>
<Style x:Key="CreationButtonStyle" TargetType="Button">
<Setter Property="Height" Value="36"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Background" Value="#303030"/>
<Setter Property="BorderBrush" Value="#505050"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="FontSize" Value="11"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Padding" Value="8,4"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
</Style>
<Style x:Key="CreationPrimaryButtonStyle"
TargetType="Button"
BasedOn="{StaticResource CreationButtonStyle}">
<Setter Property="Background" Value="#18794E"/>
<Setter Property="BorderBrush" Value="#2AA36B"/>
</Style>
<Style x:Key="CreationDangerButtonStyle"
TargetType="Button"
BasedOn="{StaticResource CreationButtonStyle}">
<Setter Property="Background" Value="#4A2626"/>
<Setter Property="BorderBrush" Value="#704040"/>
</Style>
<Style x:Key="CreationLabelStyle" TargetType="TextBlock">
<Setter Property="Foreground" Value="#9E9E9E"/>
<Setter Property="FontSize" Value="10"/>
</Style>
</UserControl.Resources>
<Grid>
<Border Width="344"
MaxHeight="720"
Margin="14"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Background="#F21C1C1C"
BorderBrush="#474747"
BorderThickness="1"
CornerRadius="12"
Padding="0">
<Border.Effect>
<DropShadowEffect BlurRadius="18"
ShadowDepth="3"
Opacity="0.35"/>
</Border.Effect>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Border Grid.Row="0"
Background="#252525"
BorderBrush="#3B3B3B"
BorderThickness="0,0,0,1"
CornerRadius="12,12,0,0"
Padding="14,12">
<StackPanel>
<TextBlock Text="+ Criar novo mapa"
Foreground="White"
FontSize="16"
FontWeight="SemiBold"/>
<TextBlock Text="Levantamento GNSS por ruas"
Foreground="#909090"
FontSize="10"
Margin="0,3,0,0"/>
</StackPanel>
</Border>
<ScrollViewer Grid.Row="1"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<StackPanel Margin="14">
<TextBlock Text="Nome do mapa"
Style="{StaticResource CreationLabelStyle}"/>
<TextBox x:Name="txtNomeMapa"
Height="34"
Margin="0,5,0,12"
Padding="8,4"
Background="#161616"
Foreground="White"
BorderBrush="#464646"
BorderThickness="1"
VerticalContentAlignment="Center"
TextChanged="txtNomeMapa_TextChanged"/>
<Border Background="#242424"
BorderBrush="#3B3B3B"
BorderThickness="1"
CornerRadius="8"
Padding="10"
Margin="0,0,0,10">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock Text="Rua atual"
Style="{StaticResource CreationLabelStyle}"/>
<TextBlock x:Name="txtRuaAtual"
Text="Rua 1"
Foreground="#48D8FF"
FontSize="18"
FontWeight="Bold"
Margin="0,2,0,0"/>
</StackPanel>
<StackPanel Grid.Column="1">
<TextBlock Text="Pontos / comprimento"
Style="{StaticResource CreationLabelStyle}"/>
<TextBlock x:Name="txtResumoRua"
Text="0 pts • 0,0 m"
Foreground="White"
FontSize="13"
FontWeight="SemiBold"
Margin="0,5,0,0"/>
</StackPanel>
</Grid>
</Border>
<Border Background="#20272A"
BorderBrush="#35505A"
BorderThickness="1"
CornerRadius="8"
Padding="10"
Margin="0,0,0,12">
<StackPanel>
<TextBlock Text="GNSS"
Foreground="#65D8FF"
FontSize="10"
FontWeight="Bold"/>
<TextBlock x:Name="txtStatusGnss"
Text="Aguardando leitura..."
Foreground="#D8D8D8"
FontSize="10"
TextWrapping="Wrap"
Margin="0,4,0,0"/>
</StackPanel>
</Border>
<TextBlock Text="Captura dos pontos"
Style="{StaticResource CreationLabelStyle}"/>
<StackPanel Orientation="Horizontal"
Margin="0,6,0,8">
<RadioButton x:Name="rbManual"
Content="Manual"
Foreground="White"
Margin="0,0,18,0"
Checked="rbModo_Checked"/>
<RadioButton x:Name="rbAutomatico"
Content="Automático"
Foreground="White"
Checked="rbModo_Checked"/>
</StackPanel>
<Button x:Name="btnRegistrarPonto"
Content="+ Registrar ponto"
Style="{StaticResource CreationPrimaryButtonStyle}"
Click="btnRegistrarPonto_Click"/>
<Button x:Name="btnCapturaAutomatica"
Content="▶ Iniciar captura automática"
Style="{StaticResource CreationPrimaryButtonStyle}"
Margin="0,7,0,0"
Click="btnCapturaAutomatica_Click"/>
<Button x:Name="btnRemoverUltimoPonto"
Content="↶ Remover último ponto"
Style="{StaticResource CreationButtonStyle}"
Margin="0,7,0,0"
Click="btnRemoverUltimoPonto_Click"/>
<Separator Margin="0,14,0,12"
Background="#404040"/>
<TextBlock Text="Ruas levantadas"
Style="{StaticResource CreationLabelStyle}"/>
<ComboBox x:Name="cmbRuas"
Height="34"
Margin="0,5,0,8"
DisplayMemberPath="Descricao"
SelectedValuePath="Numero"
SelectionChanged="cmbRuas_SelectionChanged"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Button x:Name="btnNovaRua"
Grid.Column="0"
Content="+ Nova rua"
Style="{StaticResource CreationButtonStyle}"
Click="btnNovaRua_Click"/>
<Button x:Name="btnExcluirRua"
Grid.Column="2"
Content="🗑 Excluir rua"
Style="{StaticResource CreationDangerButtonStyle}"
Click="btnExcluirRua_Click"/>
</Grid>
<Button x:Name="btnLimpar"
Content="Limpar levantamento"
Style="{StaticResource CreationDangerButtonStyle}"
Margin="0,8,0,0"
Click="btnLimpar_Click"/>
<Border Background="#222222"
BorderBrush="#373737"
BorderThickness="1"
CornerRadius="8"
Padding="10"
Margin="0,12,0,0">
<TextBlock x:Name="txtResumoTotal"
Text="1 rua • 0 pontos • 0,0 m"
Foreground="#BDBDBD"
FontSize="10"
TextWrapping="Wrap"/>
</Border>
<Grid Margin="0,14,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Button x:Name="btnCancelar"
Grid.Column="0"
Content="✕ Cancelar"
Style="{StaticResource CreationButtonStyle}"
Click="btnCancelar_Click"/>
<Button x:Name="btnSalvar"
Grid.Column="2"
Content="💾 Salvar mapa"
Style="{StaticResource CreationPrimaryButtonStyle}"
Click="btnSalvar_Click"/>
</Grid>
<TextBlock Text="Salvar gera o GeoJSON e já o carrega como mapa oficial."
Foreground="#777777"
FontSize="9"
TextWrapping="Wrap"
Margin="0,8,0,0"/>
</StackPanel>
</ScrollViewer>
</Grid>
</Border>
</Grid>
</UserControl>

View File

@ -0,0 +1,178 @@
using OperationControl.Mapas.Creation;
using System.Windows;
using System.Windows.Controls;
namespace OperationControl.Mapas.Creation.Views
{
/// <summary>
/// Cockpit passivo da criação de mapas.
/// Exibe estado e emite intenções. Não altera mapa ou GNSS.
/// </summary>
public partial class MapaCriacaoOverlay : System.Windows.Controls.UserControl
{
public event Action<string>? NomeMapaAlterado;
public event Action<MapaModoCapturaCriacao>? ModoCapturaAlterado;
public event Action? RegistrarPontoSolicitado;
public event Action? RemoverUltimoPontoSolicitado;
public event Action? IniciarCapturaAutomaticaSolicitado;
public event Action? FinalizarCapturaAutomaticaSolicitado;
public event Action? NovaRuaSolicitada;
public event Action<int>? RuaSelecionada;
public event Action<int>? ExcluirRuaSolicitada;
public event Action? LimparSolicitado;
public event Action? SalvarSolicitado;
public event Action? CancelarSolicitado;
private bool _atualizandoEstado;
private MapaResumoCriacao? _ultimoResumo;
public MapaCriacaoOverlay()
{
InitializeComponent();
}
public void AtualizarEstado(MapaResumoCriacao? resumo)
{
_atualizandoEstado = true;
try
{
_ultimoResumo = resumo;
bool ativo = resumo?.EmCriacao == true;
Visibility = ativo ? Visibility.Visible : Visibility.Collapsed;
IsHitTestVisible = ativo;
if (!ativo)
return;
string nome = resumo!.Nome ?? string.Empty;
if (!string.Equals(txtNomeMapa.Text, nome, StringComparison.Ordinal))
{
int caret = txtNomeMapa.CaretIndex;
txtNomeMapa.Text = nome;
txtNomeMapa.CaretIndex = Math.Min(caret, txtNomeMapa.Text.Length);
}
txtRuaAtual.Text = resumo.RuaAtual > 0
? $"Rua {resumo.RuaAtual}"
: "Sem rua";
txtResumoRua.Text =
$"{resumo.PontosRuaAtual} pts • {resumo.ComprimentoRuaAtualMetros:F1} m";
txtStatusGnss.Text = string.IsNullOrWhiteSpace(resumo.StatusGnss)
? "GNSS sem leitura."
: resumo.StatusGnss;
rbManual.IsChecked = resumo.ModoCaptura == MapaModoCapturaCriacao.Manual;
rbAutomatico.IsChecked = resumo.ModoCaptura == MapaModoCapturaCriacao.Automatico;
cmbRuas.ItemsSource = resumo.Ruas;
cmbRuas.SelectedValue = resumo.RuaAtual;
bool gravando = resumo.GravacaoAutomaticaAtiva;
btnRegistrarPonto.Visibility =
resumo.ModoCaptura == MapaModoCapturaCriacao.Manual
? Visibility.Visible
: Visibility.Collapsed;
btnCapturaAutomatica.Visibility =
resumo.ModoCaptura == MapaModoCapturaCriacao.Automatico
? Visibility.Visible
: Visibility.Collapsed;
btnRegistrarPonto.IsEnabled = !gravando && resumo.LeituraGnssValida;
btnCapturaAutomatica.Content = gravando
? "■ Finalizar captura automática"
: "▶ Iniciar captura automática";
btnCapturaAutomatica.IsEnabled = gravando || resumo.LeituraGnssValida;
btnRemoverUltimoPonto.IsEnabled = !gravando && resumo.PontosRuaAtual > 0;
btnNovaRua.IsEnabled = resumo.PodeNovaRua;
btnExcluirRua.IsEnabled = !gravando && resumo.QuantidadeRuas > 0;
btnLimpar.IsEnabled = !gravando && resumo.PontosTotal > 0;
cmbRuas.IsEnabled = !gravando && resumo.QuantidadeRuas > 0;
rbManual.IsEnabled = !gravando;
rbAutomatico.IsEnabled = !gravando;
txtNomeMapa.IsEnabled = !gravando;
btnSalvar.IsEnabled = resumo.PodeSalvar;
txtResumoTotal.Text =
$"{resumo.QuantidadeRuas} {(resumo.QuantidadeRuas == 1 ? "rua" : "ruas")} • " +
$"{resumo.PontosTotal} {(resumo.PontosTotal == 1 ? "ponto" : "pontos")} • " +
$"{resumo.ComprimentoTotalMetros:F1} m" +
Environment.NewLine +
$"{resumo.QuantidadeRuasValidas}/{resumo.QuantidadeRuas} ruas válidas";
}
finally
{
_atualizandoEstado = false;
}
}
private void txtNomeMapa_TextChanged(object sender, TextChangedEventArgs e)
{
if (_atualizandoEstado)
return;
NomeMapaAlterado?.Invoke(txtNomeMapa.Text);
}
private void rbModo_Checked(object sender, RoutedEventArgs e)
{
if (_atualizandoEstado)
return;
if (rbAutomatico.IsChecked == true)
ModoCapturaAlterado?.Invoke(MapaModoCapturaCriacao.Automatico);
else if (rbManual.IsChecked == true)
ModoCapturaAlterado?.Invoke(MapaModoCapturaCriacao.Manual);
}
private void btnRegistrarPonto_Click(object sender, RoutedEventArgs e) =>
RegistrarPontoSolicitado?.Invoke();
private void btnRemoverUltimoPonto_Click(object sender, RoutedEventArgs e) =>
RemoverUltimoPontoSolicitado?.Invoke();
private void btnCapturaAutomatica_Click(object sender, RoutedEventArgs e)
{
if (_ultimoResumo?.GravacaoAutomaticaAtiva == true)
FinalizarCapturaAutomaticaSolicitado?.Invoke();
else
IniciarCapturaAutomaticaSolicitado?.Invoke();
}
private void btnNovaRua_Click(object sender, RoutedEventArgs e) =>
NovaRuaSolicitada?.Invoke();
private void cmbRuas_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_atualizandoEstado)
return;
if (cmbRuas.SelectedValue is int numeroRua)
RuaSelecionada?.Invoke(numeroRua);
}
private void btnExcluirRua_Click(object sender, RoutedEventArgs e)
{
int numeroRua = _ultimoResumo?.RuaAtual ?? 0;
if (numeroRua > 0)
ExcluirRuaSolicitada?.Invoke(numeroRua);
}
private void btnLimpar_Click(object sender, RoutedEventArgs e) =>
LimparSolicitado?.Invoke();
private void btnSalvar_Click(object sender, RoutedEventArgs e) =>
SalvarSolicitado?.Invoke();
private void btnCancelar_Click(object sender, RoutedEventArgs e) =>
CancelarSolicitado?.Invoke();
}
}

View File

@ -0,0 +1,412 @@
using Mapsui;
using Mapsui.Layers;
using Mapsui.Nts;
using Mapsui.Projections;
using Mapsui.Styles;
using NetTopologySuite.Geometries;
using OperationControl.Mapas.Creation;
using Brush = Mapsui.Styles.Brush;
using Color = Mapsui.Styles.Color;
using Font = Mapsui.Styles.Font;
using Pen = Mapsui.Styles.Pen;
namespace OperationControl.Mapas.Rendering
{
/// <summary>
/// Preview georreferenciado do mapa ainda em levantamento.
///
/// Regras visuais:
/// - ruas concluídas: verde;
/// - rua atual: ciano e mais espessa;
/// - pontos da rua atual: marcadores numerados;
/// - primeira posição de cada rua: etiqueta R1, R2...
///
/// Nada aqui altera o MapaWorkspace.
/// </summary>
public sealed class MapaCriacaoRenderService
{
public const string NomeCamada =
"MapaCriacaoPreview";
public MemoryLayer CriarCamada(
MapaEstadoCriacao estado)
{
if (estado == null)
{
throw new ArgumentNullException(
nameof(estado)
);
}
return new MemoryLayer
{
Name =
NomeCamada,
Features =
CriarFeatures(
estado
)
};
}
public void AtualizarCamada(
MemoryLayer camada,
MapaEstadoCriacao estado)
{
if (camada == null)
{
throw new ArgumentNullException(
nameof(camada)
);
}
if (estado == null)
{
throw new ArgumentNullException(
nameof(estado)
);
}
camada.Features =
CriarFeatures(
estado
);
}
private static IEnumerable<IFeature>
CriarFeatures(
MapaEstadoCriacao estado)
{
var features =
new List<IFeature>();
if (!estado.EmCriacao)
{
return features;
}
for (
int indiceRua = 0;
indiceRua < estado.Ruas.Count;
indiceRua++
)
{
MapaRuaCriacao rua =
estado.Ruas[indiceRua];
if (
rua == null ||
rua.Pontos.Count == 0
)
{
continue;
}
bool atual =
indiceRua ==
estado.IndiceRuaAtual;
Color corLinha =
atual
? new Color(
0,
210,
255,
255
)
: new Color(
95,
207,
128,
255
);
/*
* LINHA DA RUA
*/
if (
rua.Pontos.Count >= 2
)
{
Coordinate[] coordenadas =
rua
.Pontos
.Select(
ponto =>
{
var mercator =
SphericalMercator
.FromLonLat(
ponto.Longitude,
ponto.Latitude
);
return new Coordinate(
mercator.x,
mercator.y
);
}
)
.ToArray();
var featureLinha =
new GeometryFeature
{
Geometry =
new LineString(
coordenadas
)
};
featureLinha["Tipo"] =
"MapaCriacaoRua";
featureLinha["Rua"] =
rua.Numero;
featureLinha.Styles.Add(
new VectorStyle
{
Line =
new Pen(
corLinha,
atual ? 5 : 3
)
}
);
features.Add(
featureLinha
);
}
/*
* MARCADOR DO INÍCIO DA RUA
*/
MapaPontoCriacao primeiro =
rua.Pontos[0];
var inicioMercator =
SphericalMercator
.FromLonLat(
primeiro.Longitude,
primeiro.Latitude
);
var marcadorRua =
new PointFeature(
inicioMercator.x,
inicioMercator.y
);
marcadorRua["Tipo"] =
"MapaCriacaoInicioRua";
marcadorRua["label"] =
$"R{rua.Numero}";
marcadorRua.Styles.Add(
new SymbolStyle
{
SymbolType =
SymbolType.Ellipse,
SymbolScale =
atual
? 1.25
: 1.0,
Fill =
new Brush(
corLinha
),
Outline =
new Pen(
Color.White,
2
)
}
);
marcadorRua.Styles.Add(
new LabelStyle
{
LabelColumn =
"label",
ForeColor =
Color.White,
BackColor =
null,
HorizontalAlignment =
LabelStyle
.HorizontalAlignmentEnum
.Center,
VerticalAlignment =
LabelStyle
.VerticalAlignmentEnum
.Center,
Font =
new Font
{
Size = 10
}
}
);
features.Add(
marcadorRua
);
/*
* PONTOS DA RUA ATUAL
*
* Nas ruas concluídas evitamos centenas de marcadores
* para manter o mapa limpo e leve.
*/
if (!atual)
{
continue;
}
for (
int indicePonto = 0;
indicePonto < rua.Pontos.Count;
indicePonto++
)
{
MapaPontoCriacao ponto =
rua.Pontos[indicePonto];
var mercator =
SphericalMercator
.FromLonLat(
ponto.Longitude,
ponto.Latitude
);
var featurePonto =
new PointFeature(
mercator.x,
mercator.y
);
featurePonto["Tipo"] =
"MapaCriacaoPonto";
featurePonto["Rua"] =
rua.Numero;
featurePonto["Ordem"] =
indicePonto + 1;
featurePonto["label"] =
(indicePonto + 1)
.ToString();
featurePonto.Styles.Add(
new SymbolStyle
{
SymbolType =
SymbolType.Ellipse,
SymbolScale =
0.95,
Fill =
new Brush(
new Color(
0,
210,
255,
255
)
),
Outline =
new Pen(
Color.White,
2
)
}
);
featurePonto.Styles.Add(
new LabelStyle
{
LabelColumn =
"label",
ForeColor =
Color.White,
BackColor =
null,
HorizontalAlignment =
LabelStyle
.HorizontalAlignmentEnum
.Center,
VerticalAlignment =
LabelStyle
.VerticalAlignmentEnum
.Center,
Font =
new Font
{
Size = 9
}
}
);
features.Add(
featurePonto
);
}
}
return features;
}
}
}

View File

@ -22,6 +22,7 @@ namespace OperationControl.ViewModels
public event PropertyChangedEventHandler? PropertyChanged;
public event Action<string, AgroBase.Models.Enums.TipoMapaOperacao>? CarregarMapaSolicitado;
public event Action? CriarMapaSolicitado;
public event Action? EditarMapaSolicitado;
// Método, latitude, longitude, altitude, offset frontal cm, offset lateral cm
@ -63,6 +64,7 @@ namespace OperationControl.ViewModels
};
public ICommand CarregarMapaCommand { get; }
public ICommand CriarMapaCommand { get; }
public ICommand EditarMapaCommand { get; }
public ICommand FixarBaseCommand { get; }
public ICommand AplicarOffsetCommand { get; }
@ -73,6 +75,7 @@ namespace OperationControl.ViewModels
public PreparacaoMapaBottomViewModel()
{
CarregarMapaCommand = new RelayCommand(_ => ExecutarCarregarMapa(), _ => PodeCarregarMapa);
CriarMapaCommand = new RelayCommand(_ => CriarMapaSolicitado?.Invoke(), _ => PodeCriarMapa);
EditarMapaCommand = new RelayCommand(_ => EditarMapaSolicitado?.Invoke(), _ => PodeEditarMapa);
FixarBaseCommand = new RelayCommand(_ => ExecutarFixarBase(), _ => PodeFixarBase);
AplicarOffsetCommand = new RelayCommand(_ => ExecutarAplicarOffset(), _ => PodeEditarOffsetBase);
@ -124,6 +127,7 @@ namespace OperationControl.ViewModels
private bool _mapaCarregado;
private bool _edicaoMapaAtiva;
private bool _criacaoMapaAtiva;
/// <summary>
/// Enquanto o editor estiver aberto nesta etapa:
@ -137,20 +141,32 @@ namespace OperationControl.ViewModels
public bool EdicaoMapaAtiva =>
_edicaoMapaAtiva;
public bool CriacaoMapaAtiva =>
_criacaoMapaAtiva;
public bool PodeCriarMapa =>
!_edicaoMapaAtiva &&
!_criacaoMapaAtiva &&
!FixacaoEmAndamento &&
!AlteracaoPapelGnssEmAndamento;
public bool PodeEditarMapa =>
_mapaCarregado &&
!_edicaoMapaAtiva &&
!_criacaoMapaAtiva &&
!FixacaoEmAndamento &&
!AlteracaoPapelGnssEmAndamento;
public bool PodeContinuar =>
_mapaCarregado &&
!_edicaoMapaAtiva &&
!_criacaoMapaAtiva &&
!FixacaoEmAndamento &&
!AlteracaoPapelGnssEmAndamento;
public bool PodeVoltar =>
!_edicaoMapaAtiva &&
!_criacaoMapaAtiva &&
!FixacaoEmAndamento &&
!AlteracaoPapelGnssEmAndamento;
@ -201,6 +217,10 @@ namespace OperationControl.ViewModels
nameof(PodeEditarMapa)
);
OnPropertyChanged(
nameof(PodeCriarMapa)
);
OnPropertyChanged(
nameof(PodeCarregarMapa)
);
@ -214,6 +234,22 @@ namespace OperationControl.ViewModels
);
}
public void DefinirCriacaoMapaAtiva(bool ativa)
{
if (_criacaoMapaAtiva == ativa)
return;
_criacaoMapaAtiva = ativa;
OnPropertyChanged(nameof(CriacaoMapaAtiva));
OnPropertyChanged(nameof(PodeCriarMapa));
OnPropertyChanged(nameof(PodeEditarMapa));
OnPropertyChanged(nameof(PodeCarregarMapa));
OnPropertyChanged(nameof(PodeContinuar));
OnPropertyChanged(nameof(PodeVoltar));
}
private GnssPapelEstacao _papelGnssAtual = GnssPapelEstacao.Indefinido;
public GnssPapelEstacao PapelGnssAtual
{
@ -316,6 +352,10 @@ namespace OperationControl.ViewModels
nameof(PodeEditarMapa)
);
OnPropertyChanged(
nameof(PodeCriarMapa)
);
OnPropertyChanged(
nameof(PodeEditarMetodoFixacao)
);
@ -451,6 +491,7 @@ namespace OperationControl.ViewModels
OnPropertyChanged(nameof(FixacaoEmAndamento));
OnPropertyChanged(nameof(PodeCarregarMapa));
OnPropertyChanged(nameof(PodeEditarMapa));
OnPropertyChanged(nameof(PodeCriarMapa));
OnPropertyChanged(nameof(PodeEditarMetodoFixacao));
OnPropertyChanged(nameof(PodeEditarCoordenadasManuais));
OnPropertyChanged(nameof(PodeEditarOffsetBase));
@ -480,6 +521,7 @@ namespace OperationControl.ViewModels
public bool PodeCarregarMapa =>
!_edicaoMapaAtiva &&
!_criacaoMapaAtiva &&
!FixacaoEmAndamento &&
!AlteracaoPapelGnssEmAndamento;

View File

@ -8,6 +8,7 @@ using Newtonsoft.Json.Linq;
using OperationControl.Controls;
using OperationControl.Helpers;
using OperationControl.Mapas;
using OperationControl.Mapas.Creation;
using OperationControl.Mapas.Editing;
using OperationControl.Mapas.Interaction;
using OperationControl.Mapas.Models;
@ -58,6 +59,7 @@ namespace OperationControl.ViewModels
_viewPreparacaoMapaBottom = new PreparacaoMapaBottomView();
_viewPreparacaoMapaBottom._vm.CarregarMapaSolicitado += OnCarregarMapaSolicitado;
_viewPreparacaoMapaBottom._vm.CriarMapaSolicitado += OnCriarMapaSolicitado;
_viewPreparacaoMapaBottom._vm.EditarMapaSolicitado += OnEditarMapaPreparacaoSolicitado;
_viewPreparacaoMapaBottom._vm.FixarBaseSolicitado += OnFixarBaseSolicitado;
_viewPreparacaoMapaBottom._vm.AplicarOffsetSolicitado += OnAplicarOffsetSolicitado;
@ -97,6 +99,47 @@ namespace OperationControl.ViewModels
.SalvarMapaEditadoSolicitado +=
OnSalvarMapaEditadoSolicitado;
var criacaoOverlay =
_viewPreparacaoMapa
.MapaPreparacao
.CriacaoOverlay;
criacaoOverlay.NomeMapaAlterado +=
nome => _mapas.DefinirNomeCriacaoMapa(nome);
criacaoOverlay.ModoCapturaAlterado +=
modo => _mapas.DefinirModoCapturaCriacao(modo);
criacaoOverlay.RegistrarPontoSolicitado +=
() => _mapas.RegistrarPontoCriacaoManual();
criacaoOverlay.RemoverUltimoPontoSolicitado +=
() => _mapas.RemoverUltimoPontoCriacao();
criacaoOverlay.IniciarCapturaAutomaticaSolicitado +=
() => _mapas.IniciarGravacaoAutomaticaCriacao();
criacaoOverlay.FinalizarCapturaAutomaticaSolicitado +=
() => _mapas.FinalizarGravacaoAutomaticaCriacao();
criacaoOverlay.NovaRuaSolicitada +=
() => _mapas.CriarNovaRuaMapa();
criacaoOverlay.RuaSelecionada +=
numeroRua => _mapas.SelecionarRuaCriacao(numeroRua);
criacaoOverlay.ExcluirRuaSolicitada +=
OnExcluirRuaCriacaoSolicitado;
criacaoOverlay.LimparSolicitado +=
OnLimparCriacaoSolicitado;
criacaoOverlay.SalvarSolicitado +=
OnSalvarMapaCriadoSolicitado;
criacaoOverlay.CancelarSolicitado +=
OnCancelarCriacaoMapaSolicitado;
_viewOperacaoLeft = new OperacaoLeftView();
_viewOperacaoRight = new OperacaoRightView();
@ -118,6 +161,7 @@ namespace OperationControl.ViewModels
_mapas.ValidacaoEdicaoFalhou += OnMapaValidacaoEdicaoFalhou;
_mapas.EstadoMapaAlterado += OnEstadoMapaAlterado;
_mapas.EstadoEdicaoAlterado += OnEstadoMapaAlterado;
_mapas.EstadoCriacaoAlterado += OnEstadoCriacaoAlterado;
// Commands
BackCommand = new RelayCommand(_ => GoBack(), _ => CanGoBack);
@ -240,7 +284,10 @@ namespace OperationControl.ViewModels
CurrentStep != DockStep.ConfigConexao &&
!(
CurrentStep == DockStep.PreparacaoMapa &&
_mapas.EdicaoAtiva
(
_mapas.EdicaoAtiva ||
_mapas.CriacaoMapaAtiva
)
);
public bool CanGoNext
@ -254,7 +301,8 @@ namespace OperationControl.ViewModels
DockStep.PreparacaoMapa =>
MapaCarregado &&
!_mapas.EdicaoAtiva,
!_mapas.EdicaoAtiva &&
!_mapas.CriacaoMapaAtiva,
_ =>
false
@ -1315,6 +1363,21 @@ namespace OperationControl.ViewModels
}
}
/*
* Snapshot GNSS compartilhado pelas ferramentas de mapa
* que fazem levantamento em campo:
*
* - borda GNSS do editor;
* - criação de mapa por ruas.
*
* O Controller faz Clone() internamente e aplica a mesma
* validação de freshness já usada pela borda.
*/
_mapas.AtualizarGnssLocal(
dados
);
_mapas.AtualizarBase(
dados,
mapaAtual
@ -1724,6 +1787,8 @@ namespace OperationControl.ViewModels
private void OnEstadoMapaAlterado()
{
MapaCarregado = _mapas.PossuiMapa;
_viewPreparacaoMapaBottom?
._vm?
.DefinirEdicaoMapaAtiva(
@ -1744,6 +1809,27 @@ namespace OperationControl.ViewModels
AtualizarEstadoMapaParametrizacao();
}
private void OnEstadoCriacaoAlterado()
{
bool ativa = _mapas.CriacaoMapaAtiva;
_viewPreparacaoMapaBottom?
._vm?
.DefinirCriacaoMapaAtiva(ativa);
_viewPreparacaoMapa?
.MapaPreparacao?
.CriacaoOverlay?
.AtualizarEstado(
_mapas.ObterResumoCriacaoMapa()
);
OnPropertyChanged(nameof(CanGoBack));
OnPropertyChanged(nameof(CanGoNext));
CommandManager.InvalidateRequerySuggested();
}
private void AtualizarEstadoMapaParametrizacao()
{
void Atualizar()
@ -1907,6 +1993,206 @@ namespace OperationControl.ViewModels
);
}
private void OnCriarMapaSolicitado()
{
try
{
if (CurrentStep != DockStep.PreparacaoMapa)
return;
if (_mapas.EdicaoAtiva || _mapas.CriacaoMapaAtiva)
return;
var dlgTipo = new OperationControl.Windows.SelecaoTipoMapa
{
Owner = Models.Variaveis.Dock
};
if (dlgTipo.ShowDialog() != true || dlgTipo.Resultado == null)
return;
MapViewControl? mapa = _viewPreparacaoMapa?.MapaPreparacao;
if (mapa == null)
return;
_mapas.IniciarCriacaoMapa(
mapa,
nome: "NovoMapa",
tipo: dlgTipo.Resultado.Value,
distanciaMinimaAutomaticaMetros: 0.50
);
_viewPreparacaoMapaBottom?._vm?.DefinirCriacaoMapaAtiva(true);
if (_viewPreparacaoMapaBottom?._vm != null)
{
_viewPreparacaoMapaBottom._vm.StatusTexto =
"Criação de mapa ativa. Registre os pontos das ruas pelo GNSS.";
}
Models.Variaveis.MostrarLog(
"[MAPA][CRIACAO][UI] Cockpit aberto na preparação."
);
}
catch (Exception ex)
{
Models.Variaveis.MostrarLog(
"[MAPA][CRIACAO][UI][ERRO] " + ex
);
MessageBox.Show(
"Não foi possível iniciar a criação do mapa." +
Environment.NewLine + Environment.NewLine + ex.Message,
"Criar mapa",
MessageBoxButton.OK,
MessageBoxImage.Error
);
}
}
private void OnExcluirRuaCriacaoSolicitado(int numeroRua)
{
var resposta = MessageBox.Show(
$"Excluir a Rua {numeroRua} do levantamento?",
"Excluir rua",
MessageBoxButton.YesNo,
MessageBoxImage.Question
);
if (resposta == MessageBoxResult.Yes)
_mapas.ExcluirRuaCriacao(numeroRua);
}
private void OnLimparCriacaoSolicitado()
{
var resposta = MessageBox.Show(
"Limpar todos os pontos e ruas levantados?",
"Limpar levantamento",
MessageBoxButton.YesNo,
MessageBoxImage.Question
);
if (resposta == MessageBoxResult.Yes)
_mapas.LimparLevantamentoCriacao();
}
private void OnCancelarCriacaoMapaSolicitado()
{
if (!_mapas.CriacaoMapaAtiva)
return;
MapaResumoCriacao resumo = _mapas.ObterResumoCriacaoMapa();
if (resumo.PontosTotal > 0)
{
var resposta = MessageBox.Show(
"Cancelar a criação do mapa?" +
Environment.NewLine + Environment.NewLine +
"Os pontos ainda não salvos serão descartados.",
"Cancelar criação",
MessageBoxButton.YesNo,
MessageBoxImage.Question
);
if (resposta != MessageBoxResult.Yes)
return;
}
_mapas.CancelarCriacaoMapa();
if (_viewPreparacaoMapaBottom?._vm != null)
{
_viewPreparacaoMapaBottom._vm.StatusTexto =
_mapas.PossuiMapa
? "Criação cancelada. O mapa anterior foi restaurado."
: "Criação cancelada.";
}
}
private void OnSalvarMapaCriadoSolicitado()
{
try
{
if (!_mapas.CriacaoMapaAtiva)
return;
MapaResumoCriacao resumo = _mapas.ObterResumoCriacaoMapa();
if (!resumo.PodeSalvar)
{
MessageBox.Show(
"Para salvar o mapa, informe um nome e garanta que " +
"todas as ruas possuam pelo menos 2 pontos.",
"Mapa incompleto",
MessageBoxButton.OK,
MessageBoxImage.Warning
);
return;
}
var dialog = new Microsoft.Win32.SaveFileDialog
{
Title = "Salvar novo mapa operacional",
Filter = "GeoJSON (*.geojson)|*.geojson",
DefaultExt = ".geojson",
AddExtension = true,
OverwritePrompt = true,
FileName = _mapas.ObterNomeArquivoSugeridoMapaCriado()
};
if (dialog.ShowDialog() != true)
return;
MapaDocumento documento =
_mapas.SalvarECarregarMapaCriado(dialog.FileName);
MapaCarregado = true;
_mapas.PrepararMapa(
_viewPreparacaoMapa?.MapaPreparacao,
modoInteracao: MapaModoInteracao.Visualizacao,
enquadrarDocumento: true
);
if (_viewPreparacaoMapaBottom?._vm != null)
{
_viewPreparacaoMapaBottom._vm.StatusTexto =
$"Mapa criado e carregado: {System.IO.Path.GetFileName(dialog.FileName)}";
}
MessageBox.Show(
"Mapa criado com sucesso." + Environment.NewLine + Environment.NewLine +
$"Ruas: {documento.QuantidadeRuas}" + Environment.NewLine +
$"Comprimento total: {documento.ComprimentoTotalMetros:F1} m" +
Environment.NewLine + Environment.NewLine +
dialog.FileName + Environment.NewLine + Environment.NewLine +
"O arquivo já está carregado como mapa oficial.",
"Mapa criado",
MessageBoxButton.OK,
MessageBoxImage.Information
);
}
catch (Exception ex)
{
Models.Variaveis.MostrarLog(
"[MAPA][CRIACAO][SALVAR][ERRO] " + ex
);
MessageBox.Show(
"Não foi possível salvar/carregar o novo mapa." +
Environment.NewLine + Environment.NewLine + ex.Message,
"Erro ao criar mapa",
MessageBoxButton.OK,
MessageBoxImage.Error
);
}
}
private void OnEditarMapaPreparacaoSolicitado()
{
try
@ -1933,6 +2219,17 @@ namespace OperationControl.ViewModels
}
if (_mapas.CriacaoMapaAtiva)
{
MessageBox.Show(
"Finalize ou cancele a criação do mapa antes de abrir o editor.",
"Criação de mapa ativa",
MessageBoxButton.OK,
MessageBoxImage.Information
);
return;
}
if (_mapas.EdicaoAtiva)
{
/*
@ -2155,6 +2452,16 @@ namespace OperationControl.ViewModels
return;
}
if (_mapas.CriacaoMapaAtiva)
{
MessageBox.Show(
"Finalize ou cancele a criação atual antes de carregar outro mapa.",
"Criação de mapa ativa",
MessageBoxButton.OK,
MessageBoxImage.Information
);
return;
}
_mapas.CarregarMapaArquivo(caminhoArquivo, tipo);
@ -2181,6 +2488,21 @@ namespace OperationControl.ViewModels
{
try
{
if (
_mapas.CapturaBordaGnssAutomaticaAtiva ||
_mapas.CapturaCriacaoAutomaticaAtiva
)
{
MessageBox.Show(
"Finalize o levantamento GNSS automático antes de alterar " +
"a configuração física da estação.",
"Levantamento em andamento",
MessageBoxButton.OK,
MessageBoxImage.Warning
);
return;
}
if (!(Models.Variaveis.GpsService.BaseFix?.FixLiberado ?? false))
{
bool fixar = false;
@ -2336,13 +2658,13 @@ namespace OperationControl.ViewModels
* durante uma captura automática da borda.
*/
if (
_mapas.CapturaBordaGnssAutomaticaAtiva
_mapas.CapturaBordaGnssAutomaticaAtiva ||
_mapas.CapturaCriacaoAutomaticaAtiva
)
{
MessageBox.Show(
"Existe um levantamento GNSS em andamento.\n\n" +
"Finalize ou descarte a borda antes de alterar " +
"o modo do receptor.",
"Existe um levantamento GNSS automático em andamento.\n\n" +
"Finalize a captura antes de alterar o modo do receptor.",
"Levantamento em andamento",
MessageBoxButton.OK,
MessageBoxImage.Warning

View File

@ -510,6 +510,14 @@
Command="{Binding CarregarMapaCommand}"
IsEnabled="{Binding PodeCarregarMapa}"/>
<Button
Content="+ Criar novo mapa"
Style="{StaticResource PanelButtonStyle}"
Command="{Binding CriarMapaCommand}"
IsEnabled="{Binding PodeCriarMapa}"
Margin="0,8,0,0"
ToolTip="Cria um mapa por levantamento GNSS, rua por rua, sem exigir Rover."/>
<Button
Content="✎ Editar mapa"
Style="{StaticResource PanelButtonStyle}"