417 lines
11 KiB
C#
417 lines
11 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public class MapaRenderService
|
|
{
|
|
public const string NomeCamadaPadrao =
|
|
"MapaOperacionalNovo";
|
|
|
|
|
|
/// <summary>
|
|
/// Cria uma nova camada visual a partir do documento.
|
|
///
|
|
/// O documento permanece sendo a fonte de verdade.
|
|
/// </summary>
|
|
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<GeometryFeature>(
|
|
documento.Ruas.Count
|
|
);
|
|
|
|
|
|
var featuresPorRuaId =
|
|
new Dictionary<string, GeometryFeature>(
|
|
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
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<string>(
|
|
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()
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Atualiza visualmente apenas uma rua.
|
|
///
|
|
/// Útil para interações pontuais, evitando percorrer
|
|
/// o mapa inteiro a cada clique.
|
|
/// </summary>
|
|
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
|
|
};
|
|
}
|
|
|
|
}
|
|
} |