1517 lines
40 KiB
C#
1517 lines
40 KiB
C#
using NetTopologySuite.Features;
|
|
using NetTopologySuite.Geometries;
|
|
using NetTopologySuite.IO;
|
|
using OperationControl.Mapas.Models;
|
|
using ProjNet.CoordinateSystems;
|
|
using ProjNet.CoordinateSystems.Transformations;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using static AgroBase.Models.Enums;
|
|
|
|
namespace OperationControl.Mapas.Services
|
|
{
|
|
public class MapaImportService
|
|
{
|
|
private readonly GeometryFactory _geometryFactoryWgs84 =
|
|
new GeometryFactory(
|
|
new PrecisionModel(),
|
|
4326
|
|
);
|
|
|
|
private sealed class FeatureOrigem
|
|
{
|
|
public int Indice { get; set; }
|
|
|
|
public Geometry Geometria { get; set; } = null!;
|
|
|
|
public Dictionary<string, object?> Atributos { get; set; }
|
|
= new(StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
|
|
private sealed class FeaturePreparada
|
|
{
|
|
public FeatureOrigem Origem { get; set; } = null!;
|
|
|
|
public string? IdOriginal { get; set; }
|
|
|
|
public List<LineString> Linhas { get; set; } = new();
|
|
}
|
|
|
|
|
|
public MapaDocumento Importar(
|
|
string caminhoArquivo,
|
|
TipoMapaOperacao tipo)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(caminhoArquivo))
|
|
{
|
|
throw new ArgumentException(
|
|
"O caminho do arquivo de mapa não foi informado.",
|
|
nameof(caminhoArquivo)
|
|
);
|
|
}
|
|
|
|
if (!File.Exists(caminhoArquivo))
|
|
{
|
|
throw new FileNotFoundException(
|
|
"O arquivo de mapa não foi encontrado.",
|
|
caminhoArquivo
|
|
);
|
|
}
|
|
|
|
string extensao =
|
|
Path.GetExtension(caminhoArquivo)
|
|
.ToLowerInvariant();
|
|
|
|
var avisos = new List<string>();
|
|
|
|
MapaDocumento documento;
|
|
|
|
switch (extensao)
|
|
{
|
|
case ".json":
|
|
case ".geojson":
|
|
|
|
documento = ImportarGeoJson(
|
|
caminhoArquivo,
|
|
tipo,
|
|
avisos
|
|
);
|
|
|
|
break;
|
|
|
|
|
|
case ".shp":
|
|
|
|
documento = ImportarShapefile(
|
|
caminhoArquivo,
|
|
tipo,
|
|
avisos
|
|
);
|
|
|
|
break;
|
|
|
|
|
|
default:
|
|
|
|
throw new InvalidOperationException(
|
|
$"Formato de mapa não suportado: '{extensao}'."
|
|
);
|
|
}
|
|
|
|
if (!documento.PossuiRuas)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"O arquivo não produziu nenhuma rua válida."
|
|
);
|
|
}
|
|
|
|
documento.AvisosImportacao = avisos;
|
|
|
|
return documento;
|
|
}
|
|
|
|
|
|
#region GEOJSON
|
|
|
|
private MapaDocumento ImportarGeoJson(
|
|
string caminhoArquivo,
|
|
TipoMapaOperacao tipo,
|
|
List<string> avisos)
|
|
{
|
|
string json =
|
|
File.ReadAllText(
|
|
caminhoArquivo,
|
|
System.Text.Encoding.UTF8
|
|
);
|
|
|
|
if (string.IsNullOrWhiteSpace(json))
|
|
{
|
|
throw new InvalidOperationException(
|
|
"O arquivo GeoJSON está vazio."
|
|
);
|
|
}
|
|
|
|
FeatureCollection featureCollection;
|
|
|
|
try
|
|
{
|
|
var reader = new GeoJsonReader();
|
|
|
|
featureCollection =
|
|
reader.Read<FeatureCollection>(json);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Não foi possível interpretar o arquivo como GeoJSON.",
|
|
ex
|
|
);
|
|
}
|
|
|
|
if (featureCollection == null ||
|
|
featureCollection.Count == 0)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"O GeoJSON não possui features."
|
|
);
|
|
}
|
|
|
|
var features = new List<FeatureOrigem>();
|
|
|
|
int indice = 0;
|
|
|
|
foreach (var feature in featureCollection)
|
|
{
|
|
indice++;
|
|
|
|
if (feature?.Geometry == null ||
|
|
feature.Geometry.IsEmpty)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
features.Add(
|
|
new FeatureOrigem
|
|
{
|
|
Indice = indice,
|
|
Geometria = feature.Geometry,
|
|
Atributos =
|
|
CopiarAtributos(
|
|
feature.Attributes
|
|
)
|
|
}
|
|
);
|
|
}
|
|
|
|
if (features.Count == 0)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"O GeoJSON não possui geometrias válidas."
|
|
);
|
|
}
|
|
|
|
/*
|
|
* GeoJSON operacional é assumido como WGS84.
|
|
*
|
|
* Não tentamos adivinhar projeções em arquivos GeoJSON.
|
|
* Se as coordenadas estiverem fora dos limites físicos
|
|
* de longitude/latitude, a importação será rejeitada.
|
|
*/
|
|
return ConstruirDocumento(
|
|
caminhoArquivo,
|
|
tipo,
|
|
MapaFormatoOrigem.GeoJson,
|
|
features,
|
|
(x, y) => (x, y),
|
|
avisos
|
|
);
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
#region SHAPEFILE
|
|
|
|
private MapaDocumento ImportarShapefile(
|
|
string caminhoArquivo,
|
|
TipoMapaOperacao tipo,
|
|
List<string> avisos)
|
|
{
|
|
ValidarArquivosShapefile(
|
|
caminhoArquivo
|
|
);
|
|
|
|
var features =
|
|
LerFeaturesShapefile(
|
|
caminhoArquivo
|
|
);
|
|
|
|
if (features.Count == 0)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"O Shapefile não possui features válidas."
|
|
);
|
|
}
|
|
|
|
var transformacao =
|
|
CriarTransformacaoShapefileParaWgs84(
|
|
caminhoArquivo,
|
|
features,
|
|
avisos
|
|
);
|
|
|
|
return ConstruirDocumento(
|
|
caminhoArquivo,
|
|
tipo,
|
|
MapaFormatoOrigem.Shapefile,
|
|
features,
|
|
transformacao,
|
|
avisos
|
|
);
|
|
}
|
|
|
|
|
|
private List<FeatureOrigem> LerFeaturesShapefile(
|
|
string caminhoShp)
|
|
{
|
|
var resultado =
|
|
new List<FeatureOrigem>();
|
|
|
|
var geometryFactory =
|
|
new GeometryFactory(
|
|
new PrecisionModel(),
|
|
0
|
|
);
|
|
|
|
using var reader =
|
|
new ShapefileDataReader(
|
|
caminhoShp,
|
|
geometryFactory
|
|
);
|
|
|
|
var header = reader.DbaseHeader;
|
|
|
|
int indice = 0;
|
|
|
|
while (reader.Read())
|
|
{
|
|
indice++;
|
|
|
|
Geometry? geometria =
|
|
reader.Geometry;
|
|
|
|
if (geometria == null ||
|
|
geometria.IsEmpty)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var atributos =
|
|
new Dictionary<string, object?>(
|
|
StringComparer.OrdinalIgnoreCase
|
|
);
|
|
|
|
for (int i = 0;
|
|
i < header.NumFields;
|
|
i++)
|
|
{
|
|
var field =
|
|
header.Fields[i];
|
|
|
|
object? valor = null;
|
|
|
|
/*
|
|
* Dependendo da implementação do reader,
|
|
* a geometria pode ocupar o ordinal zero.
|
|
*/
|
|
try
|
|
{
|
|
valor =
|
|
reader.GetValue(i + 1);
|
|
}
|
|
catch
|
|
{
|
|
try
|
|
{
|
|
valor =
|
|
reader.GetValue(i);
|
|
}
|
|
catch
|
|
{
|
|
valor = null;
|
|
}
|
|
}
|
|
|
|
if (valor is DBNull)
|
|
valor = null;
|
|
|
|
atributos[field.Name] =
|
|
valor;
|
|
}
|
|
|
|
resultado.Add(
|
|
new FeatureOrigem
|
|
{
|
|
Indice = indice,
|
|
Geometria = geometria,
|
|
Atributos = atributos
|
|
}
|
|
);
|
|
}
|
|
|
|
return resultado;
|
|
}
|
|
|
|
|
|
private static void ValidarArquivosShapefile(
|
|
string caminhoShp)
|
|
{
|
|
string caminhoDbf =
|
|
Path.ChangeExtension(
|
|
caminhoShp,
|
|
".dbf"
|
|
);
|
|
|
|
if (!File.Exists(caminhoDbf))
|
|
{
|
|
throw new FileNotFoundException(
|
|
"O arquivo .dbf correspondente ao Shapefile não foi encontrado.",
|
|
caminhoDbf
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
private Func<double, double, (double lon, double lat)>
|
|
CriarTransformacaoShapefileParaWgs84(
|
|
string caminhoShp,
|
|
IEnumerable<FeatureOrigem> features,
|
|
List<string> avisos)
|
|
{
|
|
string caminhoPrj =
|
|
Path.ChangeExtension(
|
|
caminhoShp,
|
|
".prj"
|
|
);
|
|
|
|
bool pareceGeografico =
|
|
CoordenadasParecemLongitudeLatitude(
|
|
features
|
|
);
|
|
|
|
|
|
/*
|
|
* =====================================================
|
|
* SHP SEM PRJ
|
|
* =====================================================
|
|
*/
|
|
if (!File.Exists(caminhoPrj))
|
|
{
|
|
if (pareceGeografico)
|
|
{
|
|
avisos.Add(
|
|
"O Shapefile não possui arquivo .prj. " +
|
|
"Como as coordenadas já parecem longitude/latitude, " +
|
|
"o mapa foi assumido como WGS84."
|
|
);
|
|
|
|
return (x, y) => (x, y);
|
|
}
|
|
|
|
throw new InvalidOperationException(
|
|
"O Shapefile não possui arquivo .prj e as " +
|
|
"coordenadas não parecem estar em longitude/latitude. " +
|
|
"Não é possível determinar a projeção com segurança."
|
|
);
|
|
}
|
|
|
|
|
|
string wkt =
|
|
File.ReadAllText(
|
|
caminhoPrj,
|
|
System.Text.Encoding.UTF8
|
|
);
|
|
|
|
|
|
/*
|
|
* =====================================================
|
|
* PRJ VAZIO
|
|
* =====================================================
|
|
*/
|
|
if (string.IsNullOrWhiteSpace(wkt))
|
|
{
|
|
if (pareceGeografico)
|
|
{
|
|
avisos.Add(
|
|
"O arquivo .prj está vazio. " +
|
|
"Como as coordenadas parecem longitude/latitude, " +
|
|
"o mapa foi assumido como WGS84."
|
|
);
|
|
|
|
return (x, y) => (x, y);
|
|
}
|
|
|
|
throw new InvalidOperationException(
|
|
"O arquivo .prj está vazio e as coordenadas " +
|
|
"não parecem estar em longitude/latitude."
|
|
);
|
|
}
|
|
|
|
|
|
/*
|
|
* =====================================================
|
|
* PRJ NORMAL
|
|
* =====================================================
|
|
*/
|
|
try
|
|
{
|
|
var fabricaSistemas =
|
|
new CoordinateSystemFactory();
|
|
|
|
var sistemaOrigem =
|
|
fabricaSistemas.CreateFromWkt(
|
|
wkt
|
|
);
|
|
|
|
if (sistemaOrigem == null)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"O sistema de coordenadas do .prj não pôde ser criado."
|
|
);
|
|
}
|
|
|
|
var sistemaDestino =
|
|
GeographicCoordinateSystem.WGS84;
|
|
|
|
var fabricaTransformacao =
|
|
new CoordinateTransformationFactory();
|
|
|
|
var transformacao =
|
|
fabricaTransformacao
|
|
.CreateFromCoordinateSystems(
|
|
sistemaOrigem,
|
|
sistemaDestino
|
|
);
|
|
|
|
if (transformacao?.MathTransform == null)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Não foi possível criar a transformação para WGS84."
|
|
);
|
|
}
|
|
|
|
return (x, y) =>
|
|
{
|
|
double[] convertido =
|
|
transformacao
|
|
.MathTransform
|
|
.Transform(
|
|
new[] { x, y }
|
|
);
|
|
|
|
return (
|
|
convertido[0],
|
|
convertido[1]
|
|
);
|
|
};
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
/*
|
|
* Caso que vocês já encontraram em campo:
|
|
*
|
|
* PRJ existe, está quebrado, mas o SHP já possui
|
|
* coordenadas aparentemente geográficas.
|
|
*/
|
|
if (pareceGeografico)
|
|
{
|
|
avisos.Add(
|
|
"O arquivo .prj não pôde ser interpretado. " +
|
|
"Como as coordenadas já parecem longitude/latitude, " +
|
|
"o PRJ foi ignorado. " +
|
|
$"Motivo: {ex.Message}"
|
|
);
|
|
|
|
return (x, y) => (x, y);
|
|
}
|
|
|
|
throw new InvalidOperationException(
|
|
"O arquivo .prj do Shapefile não pôde ser interpretado " +
|
|
"e as coordenadas não parecem estar em longitude/latitude. " +
|
|
"Não é possível determinar a projeção com segurança.",
|
|
ex
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
private static bool CoordenadasParecemLongitudeLatitude(
|
|
IEnumerable<FeatureOrigem> features)
|
|
{
|
|
int quantidade = 0;
|
|
|
|
foreach (var feature in features)
|
|
{
|
|
if (feature?.Geometria == null ||
|
|
feature.Geometria.IsEmpty)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (
|
|
Coordinate coordenada
|
|
in feature.Geometria.Coordinates
|
|
)
|
|
{
|
|
double x = coordenada.X;
|
|
double y = coordenada.Y;
|
|
|
|
if (!double.IsFinite(x) ||
|
|
!double.IsFinite(y))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (x < -180.0 ||
|
|
x > 180.0 ||
|
|
y < -90.0 ||
|
|
y > 90.0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
quantidade++;
|
|
}
|
|
}
|
|
|
|
return quantidade > 0;
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
#region CONSTRUCAO DO DOCUMENTO
|
|
|
|
private MapaDocumento ConstruirDocumento(
|
|
string caminhoArquivo,
|
|
TipoMapaOperacao tipo,
|
|
MapaFormatoOrigem formato,
|
|
IEnumerable<FeatureOrigem> features,
|
|
Func<double, double, (double lon, double lat)> transformar,
|
|
List<string> avisos)
|
|
{
|
|
var documento =
|
|
new MapaDocumento
|
|
{
|
|
Nome =
|
|
Path.GetFileNameWithoutExtension(
|
|
caminhoArquivo
|
|
),
|
|
|
|
CaminhoOrigem =
|
|
Path.GetFullPath(
|
|
caminhoArquivo
|
|
),
|
|
|
|
FormatoOrigem = formato,
|
|
|
|
Tipo = tipo,
|
|
|
|
Modificado = false
|
|
};
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* 1. PREPARA AS FEATURES ANTES DE ATRIBUIR IDs
|
|
* ============================================================
|
|
*
|
|
* Esse passo é proposital.
|
|
*
|
|
* Primeiro precisamos conhecer TODOS os IDs existentes no mapa.
|
|
* Só depois podemos começar a gerar IDs automáticos.
|
|
*
|
|
* Assim evitamos este caso:
|
|
*
|
|
* Feature 1 -> sem ID
|
|
* Feature 2 -> ID original "1"
|
|
*
|
|
* O gerador NÃO pode entregar "1" para a Feature 1 antes
|
|
* de descobrir que esse ID pertence originalmente à Feature 2.
|
|
*/
|
|
|
|
var featuresPreparadas =
|
|
new List<FeaturePreparada>();
|
|
|
|
|
|
foreach (var feature in features)
|
|
{
|
|
if (feature?.Geometria == null ||
|
|
feature.Geometria.IsEmpty)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
|
|
string? idOriginal =
|
|
NormalizarIdOriginal(
|
|
ObterAtributoTexto(
|
|
feature.Atributos,
|
|
"Id",
|
|
"ID",
|
|
"id",
|
|
"OBJECTID",
|
|
"ObjectID",
|
|
"FID"
|
|
)
|
|
);
|
|
|
|
|
|
var linhas =
|
|
ExtrairLineStrings(
|
|
feature.Geometria
|
|
)
|
|
.Where(x =>
|
|
x != null &&
|
|
!x.IsEmpty &&
|
|
x.NumPoints >= 2
|
|
)
|
|
.ToList();
|
|
|
|
|
|
if (linhas.Count == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
|
|
featuresPreparadas.Add(
|
|
new FeaturePreparada
|
|
{
|
|
Origem = feature,
|
|
|
|
IdOriginal = idOriginal,
|
|
|
|
Linhas = linhas
|
|
}
|
|
);
|
|
}
|
|
|
|
|
|
if (featuresPreparadas.Count == 0)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"O mapa não contém nenhuma LineString navegável."
|
|
);
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* 2. CONTA AS OCORRÊNCIAS DOS IDs ORIGINAIS
|
|
* ============================================================
|
|
*
|
|
* Somente IDs que aparecem em UMA feature podem ser
|
|
* preservados como IDs canônicos.
|
|
*/
|
|
|
|
var ocorrenciasIdsOriginais =
|
|
featuresPreparadas
|
|
.Where(x =>
|
|
!string.IsNullOrWhiteSpace(
|
|
x.IdOriginal
|
|
)
|
|
)
|
|
.GroupBy(
|
|
x => x.IdOriginal!,
|
|
StringComparer.OrdinalIgnoreCase
|
|
)
|
|
.ToDictionary(
|
|
grupo => grupo.Key,
|
|
grupo => grupo.Count(),
|
|
StringComparer.OrdinalIgnoreCase
|
|
);
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* 3. RESERVA TODOS OS IDs EXISTENTES NO ARQUIVO
|
|
* ============================================================
|
|
*
|
|
* Mesmo um ID duplicado fica reservado.
|
|
*
|
|
* Exemplo:
|
|
*
|
|
* original:
|
|
* 5
|
|
* 5
|
|
* sem ID
|
|
*
|
|
* O gerador nunca poderá entregar "5" para a terceira rua.
|
|
*/
|
|
|
|
var idsOriginaisReservados =
|
|
new HashSet<string>(
|
|
ocorrenciasIdsOriginais.Keys,
|
|
StringComparer.OrdinalIgnoreCase
|
|
);
|
|
|
|
|
|
/*
|
|
* IDs canônicos já efetivamente atribuídos às ruas.
|
|
*/
|
|
var idsUsados =
|
|
new HashSet<string>(
|
|
StringComparer.OrdinalIgnoreCase
|
|
);
|
|
|
|
|
|
int proximoIdNumerico = 1;
|
|
|
|
int quantidadeIdsGerados = 0;
|
|
|
|
int idsGeradosSemOriginal = 0;
|
|
|
|
int idsGeradosPorDuplicidade = 0;
|
|
|
|
int idsGeradosPorMultipart = 0;
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* 4. CONSTRÓI AS RUAS
|
|
* ============================================================
|
|
*/
|
|
|
|
foreach (var preparada in featuresPreparadas)
|
|
{
|
|
FeatureOrigem feature =
|
|
preparada.Origem;
|
|
|
|
|
|
bool possuiIdOriginal =
|
|
!string.IsNullOrWhiteSpace(
|
|
preparada.IdOriginal
|
|
);
|
|
|
|
|
|
bool idOriginalEhUnico =
|
|
possuiIdOriginal &&
|
|
ocorrenciasIdsOriginais.TryGetValue(
|
|
preparada.IdOriginal!,
|
|
out int ocorrencias
|
|
) &&
|
|
ocorrencias == 1;
|
|
|
|
|
|
/*
|
|
* Uma Feature pode ser MultiLineString.
|
|
*
|
|
* Nesse caso, mesmo com ID original único, apenas a
|
|
* primeira LineString pode herdar esse ID.
|
|
*
|
|
* As demais precisam de IDs próprios.
|
|
*/
|
|
bool idOriginalConsumido = false;
|
|
|
|
|
|
foreach (
|
|
LineString linhaOrigem
|
|
in preparada.Linhas
|
|
)
|
|
{
|
|
LineString linhaWgs84 =
|
|
TransformarLinhaParaWgs84(
|
|
linhaOrigem,
|
|
transformar,
|
|
feature.Indice
|
|
);
|
|
|
|
|
|
string id;
|
|
|
|
bool idGerado;
|
|
|
|
|
|
/*
|
|
* ====================================================
|
|
* PRESERVA ID ORIGINAL VÁLIDO E ÚNICO
|
|
* ====================================================
|
|
*/
|
|
if (
|
|
idOriginalEhUnico &&
|
|
!idOriginalConsumido
|
|
)
|
|
{
|
|
id =
|
|
preparada.IdOriginal!;
|
|
|
|
/*
|
|
* Isso teoricamente nunca deve falhar,
|
|
* pois já verificamos unicidade.
|
|
*
|
|
* Mesmo assim deixamos uma guarda forte.
|
|
*/
|
|
if (!idsUsados.Add(id))
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Falha interna ao reservar o ID original '{id}'. " +
|
|
"O ID já havia sido atribuído a outra rua."
|
|
);
|
|
}
|
|
|
|
|
|
idGerado = false;
|
|
|
|
idOriginalConsumido = true;
|
|
}
|
|
else
|
|
{
|
|
/*
|
|
* =================================================
|
|
* GERA ID NUMÉRICO
|
|
* =================================================
|
|
*
|
|
* O gerador evita:
|
|
*
|
|
* - IDs já utilizados;
|
|
* - TODOS os IDs existentes originalmente no mapa.
|
|
*/
|
|
|
|
id =
|
|
GerarIdNumericoUnico(
|
|
idsUsados,
|
|
idsOriginaisReservados,
|
|
ref proximoIdNumerico
|
|
);
|
|
|
|
|
|
idGerado = true;
|
|
|
|
quantidadeIdsGerados++;
|
|
|
|
|
|
if (!possuiIdOriginal)
|
|
{
|
|
idsGeradosSemOriginal++;
|
|
}
|
|
else if (!idOriginalEhUnico)
|
|
{
|
|
idsGeradosPorDuplicidade++;
|
|
}
|
|
else
|
|
{
|
|
/*
|
|
* ID original era único, mas já foi usado
|
|
* pela primeira parte de uma MultiLineString.
|
|
*/
|
|
idsGeradosPorMultipart++;
|
|
}
|
|
}
|
|
|
|
|
|
string nome =
|
|
ObterAtributoTexto(
|
|
feature.Atributos,
|
|
"Name",
|
|
"NAME",
|
|
"Nome",
|
|
"NOME",
|
|
"LAYER"
|
|
)
|
|
?? $"Rua {id}";
|
|
|
|
|
|
double dist1 =
|
|
ObterAtributoDouble(
|
|
feature.Atributos,
|
|
"Dist1",
|
|
"DIST1"
|
|
);
|
|
|
|
|
|
double dist2 =
|
|
ObterAtributoDouble(
|
|
feature.Atributos,
|
|
"Dist2",
|
|
"DIST2"
|
|
);
|
|
|
|
|
|
double comprimento =
|
|
CalcularComprimentoMetros(
|
|
linhaWgs84
|
|
);
|
|
|
|
|
|
documento.Ruas.Add(
|
|
new MapaRua
|
|
{
|
|
Id = id,
|
|
|
|
IdOriginal =
|
|
preparada.IdOriginal,
|
|
|
|
IdGeradoAutomaticamente =
|
|
idGerado,
|
|
|
|
Nome = nome,
|
|
|
|
Geometria =
|
|
linhaWgs84,
|
|
|
|
ComprimentoMetros =
|
|
comprimento,
|
|
|
|
Dist1 =
|
|
dist1,
|
|
|
|
Dist2 =
|
|
dist2,
|
|
|
|
IndiceFeatureOrigem =
|
|
feature.Indice
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* 5. REGISTRA DIAGNÓSTICO
|
|
* ============================================================
|
|
*/
|
|
|
|
if (quantidadeIdsGerados > 0)
|
|
{
|
|
var motivos =
|
|
new List<string>();
|
|
|
|
|
|
if (idsGeradosSemOriginal > 0)
|
|
{
|
|
motivos.Add(
|
|
$"sem ID original: {idsGeradosSemOriginal}"
|
|
);
|
|
}
|
|
|
|
|
|
if (idsGeradosPorDuplicidade > 0)
|
|
{
|
|
motivos.Add(
|
|
$"ID original duplicado: {idsGeradosPorDuplicidade}"
|
|
);
|
|
}
|
|
|
|
|
|
if (idsGeradosPorMultipart > 0)
|
|
{
|
|
motivos.Add(
|
|
$"partes adicionais de geometria multipart: {idsGeradosPorMultipart}"
|
|
);
|
|
}
|
|
|
|
|
|
string detalhe =
|
|
motivos.Count > 0
|
|
? $" ({string.Join(", ", motivos)})"
|
|
: string.Empty;
|
|
|
|
|
|
avisos.Add(
|
|
$"{quantidadeIdsGerados} rua(s) receberam " +
|
|
"IDs numéricos automaticamente" +
|
|
detalhe +
|
|
"."
|
|
);
|
|
}
|
|
|
|
|
|
if (documento.Ruas.Count == 0)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"O mapa não contém nenhuma LineString navegável."
|
|
);
|
|
}
|
|
|
|
|
|
return documento;
|
|
}
|
|
|
|
|
|
private LineString TransformarLinhaParaWgs84(
|
|
LineString linha,
|
|
Func<double, double, (double lon, double lat)> transformar,
|
|
int indiceFeatureOrigem)
|
|
{
|
|
var coordenadas =
|
|
new List<Coordinate>();
|
|
|
|
|
|
foreach (
|
|
Coordinate coordenada
|
|
in linha.Coordinates
|
|
)
|
|
{
|
|
var convertido =
|
|
transformar(
|
|
coordenada.X,
|
|
coordenada.Y
|
|
);
|
|
|
|
|
|
ValidarCoordenadaWgs84(
|
|
convertido.lon,
|
|
convertido.lat,
|
|
indiceFeatureOrigem
|
|
);
|
|
|
|
|
|
coordenadas.Add(
|
|
new Coordinate(
|
|
convertido.lon,
|
|
convertido.lat
|
|
)
|
|
);
|
|
}
|
|
|
|
|
|
if (coordenadas.Count < 2)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"A feature {indiceFeatureOrigem} não possui " +
|
|
"pontos suficientes após a transformação."
|
|
);
|
|
}
|
|
|
|
|
|
return _geometryFactoryWgs84
|
|
.CreateLineString(
|
|
coordenadas.ToArray()
|
|
);
|
|
}
|
|
|
|
|
|
private static void ValidarCoordenadaWgs84(
|
|
double longitude,
|
|
double latitude,
|
|
int indiceFeatureOrigem)
|
|
{
|
|
bool valida =
|
|
double.IsFinite(longitude) &&
|
|
double.IsFinite(latitude) &&
|
|
longitude >= -180.0 &&
|
|
longitude <= 180.0 &&
|
|
latitude >= -90.0 &&
|
|
latitude <= 90.0;
|
|
|
|
|
|
if (!valida)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"A feature {indiceFeatureOrigem} produziu uma " +
|
|
"coordenada WGS84 inválida: " +
|
|
$"longitude={longitude}, latitude={latitude}."
|
|
);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
#region GEOMETRIAS
|
|
|
|
private static IEnumerable<LineString>
|
|
ExtrairLineStrings(
|
|
Geometry geometria)
|
|
{
|
|
if (geometria == null ||
|
|
geometria.IsEmpty)
|
|
{
|
|
yield break;
|
|
}
|
|
|
|
|
|
if (geometria is LineString linha)
|
|
{
|
|
yield return linha;
|
|
yield break;
|
|
}
|
|
|
|
|
|
if (geometria is MultiLineString multiLinha)
|
|
{
|
|
for (
|
|
int i = 0;
|
|
i < multiLinha.NumGeometries;
|
|
i++)
|
|
{
|
|
Geometry parte =
|
|
multiLinha.GetGeometryN(i);
|
|
|
|
foreach (
|
|
var linhaInterna
|
|
in ExtrairLineStrings(parte)
|
|
)
|
|
{
|
|
yield return linhaInterna;
|
|
}
|
|
}
|
|
|
|
yield break;
|
|
}
|
|
|
|
|
|
if (geometria is GeometryCollection colecao)
|
|
{
|
|
for (
|
|
int i = 0;
|
|
i < colecao.NumGeometries;
|
|
i++)
|
|
{
|
|
Geometry parte =
|
|
colecao.GetGeometryN(i);
|
|
|
|
foreach (
|
|
var linhaInterna
|
|
in ExtrairLineStrings(parte)
|
|
)
|
|
{
|
|
yield return linhaInterna;
|
|
}
|
|
}
|
|
|
|
yield break;
|
|
}
|
|
|
|
|
|
throw new InvalidOperationException(
|
|
"O mapa contém uma geometria não suportada: " +
|
|
$"{geometria.GeometryType}. " +
|
|
"São aceitas somente geometrias lineares."
|
|
);
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
#region IDS
|
|
|
|
private static string? NormalizarIdOriginal(string? idOriginal)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(idOriginal))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return idOriginal.Trim();
|
|
}
|
|
|
|
|
|
private static string GerarIdNumericoUnico(
|
|
HashSet<string> idsUsados,
|
|
HashSet<string> idsOriginaisReservados,
|
|
ref int proximoIdNumerico)
|
|
{
|
|
while (true)
|
|
{
|
|
string candidato =
|
|
proximoIdNumerico.ToString(
|
|
CultureInfo.InvariantCulture
|
|
);
|
|
|
|
|
|
proximoIdNumerico++;
|
|
|
|
|
|
/*
|
|
* Não podemos usar um ID que:
|
|
*
|
|
* 1. já foi atribuído;
|
|
* 2. existia originalmente em qualquer ponto do mapa.
|
|
*/
|
|
if (idsUsados.Contains(candidato))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
|
|
if (idsOriginaisReservados.Contains(candidato))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
|
|
idsUsados.Add(candidato);
|
|
|
|
return candidato;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
#region ATRIBUTOS
|
|
|
|
private static Dictionary<string, object?>
|
|
CopiarAtributos(
|
|
IAttributesTable? atributos)
|
|
{
|
|
var resultado =
|
|
new Dictionary<string, object?>(
|
|
StringComparer.OrdinalIgnoreCase
|
|
);
|
|
|
|
|
|
if (atributos == null)
|
|
{
|
|
return resultado;
|
|
}
|
|
|
|
|
|
foreach (
|
|
string nome
|
|
in atributos.GetNames()
|
|
)
|
|
{
|
|
try
|
|
{
|
|
object? valor =
|
|
atributos[nome];
|
|
|
|
if (valor is DBNull)
|
|
valor = null;
|
|
|
|
resultado[nome] =
|
|
valor;
|
|
}
|
|
catch
|
|
{
|
|
/*
|
|
* Um atributo estranho não deve impedir
|
|
* a leitura da geometria.
|
|
*/
|
|
}
|
|
}
|
|
|
|
|
|
return resultado;
|
|
}
|
|
|
|
|
|
private static object?
|
|
ObterAtributo(
|
|
IReadOnlyDictionary<string, object?> atributos,
|
|
params string[] nomes)
|
|
{
|
|
if (atributos == null)
|
|
return null;
|
|
|
|
|
|
foreach (string nome in nomes)
|
|
{
|
|
if (!atributos.TryGetValue(
|
|
nome,
|
|
out object? valor))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
|
|
if (valor == null ||
|
|
valor is DBNull)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
|
|
return valor;
|
|
}
|
|
|
|
|
|
return null;
|
|
}
|
|
|
|
|
|
private static string?
|
|
ObterAtributoTexto(
|
|
IReadOnlyDictionary<string, object?> atributos,
|
|
params string[] nomes)
|
|
{
|
|
object? valor =
|
|
ObterAtributo(
|
|
atributos,
|
|
nomes
|
|
);
|
|
|
|
|
|
if (valor == null)
|
|
return null;
|
|
|
|
|
|
string? texto =
|
|
Convert.ToString(
|
|
valor,
|
|
CultureInfo.InvariantCulture
|
|
);
|
|
|
|
|
|
return string.IsNullOrWhiteSpace(texto)
|
|
? null
|
|
: texto.Trim();
|
|
}
|
|
|
|
|
|
private static double
|
|
ObterAtributoDouble(
|
|
IReadOnlyDictionary<string, object?> atributos,
|
|
params string[] nomes)
|
|
{
|
|
object? valor =
|
|
ObterAtributo(
|
|
atributos,
|
|
nomes
|
|
);
|
|
|
|
|
|
if (valor == null)
|
|
return 0.0;
|
|
|
|
|
|
try
|
|
{
|
|
return Convert.ToDouble(
|
|
valor,
|
|
CultureInfo.InvariantCulture
|
|
);
|
|
}
|
|
catch
|
|
{
|
|
string? texto =
|
|
valor.ToString();
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(texto))
|
|
return 0.0;
|
|
|
|
|
|
if (double.TryParse(
|
|
texto,
|
|
NumberStyles.Any,
|
|
CultureInfo.InvariantCulture,
|
|
out double resultado))
|
|
{
|
|
return resultado;
|
|
}
|
|
|
|
|
|
if (double.TryParse(
|
|
texto,
|
|
NumberStyles.Any,
|
|
CultureInfo.GetCultureInfo("pt-BR"),
|
|
out resultado))
|
|
{
|
|
return resultado;
|
|
}
|
|
}
|
|
|
|
|
|
return 0.0;
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
#region DISTANCIA
|
|
|
|
private static double CalcularComprimentoMetros(
|
|
LineString linha)
|
|
{
|
|
if (linha == null ||
|
|
linha.NumPoints < 2)
|
|
{
|
|
return 0.0;
|
|
}
|
|
|
|
|
|
double distancia = 0.0;
|
|
|
|
Coordinate[] coordenadas =
|
|
linha.Coordinates;
|
|
|
|
|
|
for (
|
|
int i = 1;
|
|
i < coordenadas.Length;
|
|
i++)
|
|
{
|
|
Coordinate anterior =
|
|
coordenadas[i - 1];
|
|
|
|
Coordinate atual =
|
|
coordenadas[i];
|
|
|
|
|
|
distancia +=
|
|
CalcularDistanciaHaversine(
|
|
anterior.Y,
|
|
anterior.X,
|
|
atual.Y,
|
|
atual.X
|
|
);
|
|
}
|
|
|
|
|
|
return distancia;
|
|
}
|
|
|
|
|
|
private static double CalcularDistanciaHaversine(
|
|
double latitude1,
|
|
double longitude1,
|
|
double latitude2,
|
|
double longitude2)
|
|
{
|
|
const double raioTerraMetros =
|
|
6371000.0;
|
|
|
|
|
|
static double ParaRadianos(
|
|
double graus)
|
|
{
|
|
return graus *
|
|
Math.PI /
|
|
180.0;
|
|
}
|
|
|
|
|
|
double dLat =
|
|
ParaRadianos(
|
|
latitude2 -
|
|
latitude1
|
|
);
|
|
|
|
double dLon =
|
|
ParaRadianos(
|
|
longitude2 -
|
|
longitude1
|
|
);
|
|
|
|
|
|
double lat1 =
|
|
ParaRadianos(
|
|
latitude1
|
|
);
|
|
|
|
double lat2 =
|
|
ParaRadianos(
|
|
latitude2
|
|
);
|
|
|
|
|
|
double a =
|
|
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);
|
|
|
|
|
|
double c =
|
|
2.0 *
|
|
Math.Atan2(
|
|
Math.Sqrt(a),
|
|
Math.Sqrt(1.0 - a)
|
|
);
|
|
|
|
|
|
return raioTerraMetros * c;
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
} |