using Mapsui.Layers;
using Mapsui.Nts;
using Mapsui.Providers;
using Mapsui.Styles;
using OperationControl.Mapas.Models;
using Brush = Mapsui.Styles.Brush;
using Color = Mapsui.Styles.Color;
using Pen = Mapsui.Styles.Pen;
namespace OperationControl.Mapas.Rendering
{
///
/// Converte o modelo canônico de mapa em uma representação
/// visual compatível com Mapsui.
///
/// Este serviço NÃO altera o MapaDocumento.
/// Este serviço NÃO possui estado operacional.
/// Este serviço NÃO conhece telas WPF.
///
public class MapaRenderService
{
public const string NomeCamadaPadrao =
"MapaOperacionalNovo";
///
/// Cria uma nova camada visual a partir do documento.
///
/// O documento permanece sendo a fonte de verdade.
///
public MapaRenderResult CriarCamada(
MapaDocumento documento,
string? nomeCamada = null)
{
if (documento == null)
{
throw new ArgumentNullException(
nameof(documento)
);
}
if (documento.Ruas == null ||
documento.Ruas.Count == 0)
{
throw new InvalidOperationException(
"O documento não possui ruas para renderização."
);
}
var features =
new List(
documento.Ruas.Count
);
var featuresPorRuaId =
new Dictionary(
StringComparer.OrdinalIgnoreCase
);
foreach (
MapaRua rua
in documento.Ruas)
{
if (rua == null)
continue;
if (string.IsNullOrWhiteSpace(
rua.Id))
{
throw new InvalidOperationException(
"Foi encontrada uma rua sem ID durante a renderização."
);
}
if (rua.Geometria == null ||
rua.Geometria.IsEmpty)
{
throw new InvalidOperationException(
$"A rua '{rua.Id}' não possui geometria válida."
);
}
/*
* IMPORTANTÍSSIMO:
*
* A GeometryFeature recebe uma cópia da geometria.
*
* Assim nenhuma eventual manipulação realizada
* pelo componente visual poderá alterar acidentalmente
* a geometria canônica do MapaDocumento.
*/
var geometriaVisual =
rua.Geometria.Copy();
var feature =
new GeometryFeature
{
Geometry =
geometriaVisual
};
/*
* Esses atributos existem SOMENTE para facilitar:
*
* - identificação visual;
* - hit testing;
* - depuração;
* - futura interação.
*
* Eles NÃO são fonte da verdade.
*/
feature["RuaId"] =
rua.Id;
feature["Id"] =
rua.Id;
feature["Nome"] =
rua.Nome;
feature["ComprimentoMetros"] =
rua.ComprimentoMetros;
/*
* Toda feature nasce visualmente como rua normal.
*
* O estado visual não é armazenado na feature.
* O estilo é apenas uma projeção do estado real.
*/
feature.Styles.Add(
CriarEstiloNormal()
);
if (!featuresPorRuaId.TryAdd(
rua.Id,
feature))
{
throw new InvalidOperationException(
$"O ID de rua '{rua.Id}' está duplicado " +
"durante a renderização."
);
}
features.Add(
feature
);
}
if (features.Count == 0)
{
throw new InvalidOperationException(
"Nenhuma feature visual pôde ser criada."
);
}
/*
* As geometrias do nosso documento são sempre:
*
* EPSG:4326
* X = longitude
* Y = latitude
*/
var providerWgs84 =
new MemoryProvider(
features
)
{
CRS =
"EPSG:4326"
};
/*
* O Mapsui trabalha visualmente em WebMercator.
*
* A reprojeção fica SOMENTE nesta camada visual.
*/
var providerWebMercator =
new ProjectingProvider(
providerWgs84
)
{
CRS =
"EPSG:3857"
};
string nome =
string.IsNullOrWhiteSpace(
nomeCamada)
? NomeCamadaPadrao
: nomeCamada.Trim();
var layer =
new Layer(
nome
)
{
DataSource =
providerWebMercator,
/*
* Os estilos pertencem às features visuais.
*
* Isso permitirá que cada rua tenha seu próprio
* estado visual sem duplicar camadas.
*/
Style = null
};
return new MapaRenderResult(
layer,
featuresPorRuaId
);
}
///
/// Atualiza a representação visual das ruas de acordo
/// com a seleção operacional atual.
///
/// IMPORTANTE:
/// este método não altera MapaDocumento nem MapaSelecaoOperacao.
///
/// Ele apenas faz o Mapsui refletir visualmente o estado real.
///
public void AplicarSelecao(
MapaRenderResult render,
MapaSelecaoOperacao selecao)
{
if (render == null)
{
throw new ArgumentNullException(
nameof(render)
);
}
if (selecao == null)
{
throw new ArgumentNullException(
nameof(selecao)
);
}
var selecionadas =
new HashSet(
selecao.RuasOrdenadas,
StringComparer.OrdinalIgnoreCase
);
foreach (
var item
in render.FeaturesPorRuaId)
{
string ruaId =
item.Key;
GeometryFeature feature =
item.Value;
bool selecionada =
selecionadas.Contains(
ruaId
);
feature.Styles.Clear();
feature.Styles.Add(
selecionada
? CriarEstiloSelecionado()
: CriarEstiloNormal()
);
}
}
///
/// Atualiza visualmente apenas uma rua.
///
/// Útil para interações pontuais, evitando percorrer
/// o mapa inteiro a cada clique.
///
public void AtualizarEstiloRua(
MapaRenderResult render,
MapaSelecaoOperacao selecao,
string ruaId)
{
if (render == null)
{
throw new ArgumentNullException(
nameof(render)
);
}
if (selecao == null)
{
throw new ArgumentNullException(
nameof(selecao)
);
}
if (string.IsNullOrWhiteSpace(
ruaId))
{
throw new ArgumentException(
"O ID da rua não foi informado.",
nameof(ruaId)
);
}
string id =
ruaId.Trim();
GeometryFeature? feature =
render.ObterFeature(
id
);
if (feature == null)
{
throw new InvalidOperationException(
$"A rua '{id}' não possui representação visual."
);
}
bool selecionada =
selecao.RuasOrdenadas.Contains(
id,
StringComparer.OrdinalIgnoreCase
);
feature.Styles.Clear();
feature.Styles.Add(
selecionada
? CriarEstiloSelecionado()
: CriarEstiloNormal()
);
}
private static VectorStyle CriarEstiloNormal()
{
return new VectorStyle
{
Line =
new Pen(
Color.FromArgb(
255,
46,
204,
113
),
2
),
Fill =
new Brush(
Color.FromArgb(
60,
46,
204,
113
)
)
};
}
private static VectorStyle CriarEstiloSelecionado()
{
return new VectorStyle
{
Line =
new Pen(
Color.FromArgb(
255,
241,
196,
15
),
4
),
Fill = null
};
}
}
}