pequenos ajustes no operation control e controle de potencia na bomba pressurizadora
This commit is contained in:
parent
3910ae2d35
commit
fc95b86f14
|
|
@ -1,38 +1,36 @@
|
||||||
using System.IO;
|
using AgroBase.Models;
|
||||||
using System.Windows;
|
|
||||||
using BruTile;
|
using BruTile;
|
||||||
using BruTile.MbTiles;
|
using BruTile.MbTiles;
|
||||||
using BruTile.Predefined;
|
using BruTile.Predefined;
|
||||||
using SQLite;
|
|
||||||
|
|
||||||
using NetTopologySuite.IO;
|
|
||||||
using NetTopologySuite.Features;
|
|
||||||
using NetTopologySuite.Geometries;
|
|
||||||
|
|
||||||
using Mapsui;
|
using Mapsui;
|
||||||
|
using Mapsui.Extensions;
|
||||||
using Mapsui.Layers;
|
using Mapsui.Layers;
|
||||||
|
using Mapsui.Manipulations;
|
||||||
|
using Mapsui.Nts;
|
||||||
|
using Mapsui.Projections;
|
||||||
using Mapsui.Providers;
|
using Mapsui.Providers;
|
||||||
using Mapsui.Styles;
|
using Mapsui.Styles;
|
||||||
using Mapsui.Projections;
|
|
||||||
using Mapsui.Tiling.Layers;
|
using Mapsui.Tiling.Layers;
|
||||||
using Mapsui.Nts;
|
|
||||||
using Mapsui.UI.Wpf;
|
using Mapsui.UI.Wpf;
|
||||||
|
using NetTopologySuite.Features;
|
||||||
// Aliases para evitar conflito com NetTopologySuite.* (NTS)
|
using NetTopologySuite.Geometries;
|
||||||
using UserControl = System.Windows.Controls.UserControl;
|
using NetTopologySuite.IO;
|
||||||
using OpenFileDialog = Microsoft.Win32.OpenFileDialog;
|
using OperationControl.Models;
|
||||||
using MIFeature = Mapsui.IFeature;
|
using SQLite;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using static AgroBase.Models.Enums;
|
||||||
using Brush = Mapsui.Styles.Brush;
|
using Brush = Mapsui.Styles.Brush;
|
||||||
using Pen = Mapsui.Styles.Pen;
|
|
||||||
using Color = Mapsui.Styles.Color;
|
using Color = Mapsui.Styles.Color;
|
||||||
using Font = Mapsui.Styles.Font;
|
using Font = Mapsui.Styles.Font;
|
||||||
|
using MIFeature = Mapsui.IFeature;
|
||||||
using NFeature = NetTopologySuite.Features.Feature;
|
using NFeature = NetTopologySuite.Features.Feature;
|
||||||
using Mapsui.Manipulations;
|
using OpenFileDialog = Microsoft.Win32.OpenFileDialog;
|
||||||
using Mapsui.Extensions;
|
using Pen = Mapsui.Styles.Pen;
|
||||||
using OperationControl.Models;
|
// Aliases para evitar conflito com NetTopologySuite.* (NTS)
|
||||||
using static AgroBase.Models.Enums;
|
using UserControl = System.Windows.Controls.UserControl;
|
||||||
using System.Windows.Input;
|
|
||||||
using AgroBase.Models;
|
|
||||||
|
|
||||||
|
|
||||||
namespace OperationControl.Controls
|
namespace OperationControl.Controls
|
||||||
|
|
@ -985,7 +983,6 @@ namespace OperationControl.Controls
|
||||||
{
|
{
|
||||||
foiGeradoAutomaticamente = false;
|
foiGeradoAutomaticamente = false;
|
||||||
|
|
||||||
// tenta achar algum campo comum de ID
|
|
||||||
string? idOriginal =
|
string? idOriginal =
|
||||||
GetAttr(feature, "Id") ??
|
GetAttr(feature, "Id") ??
|
||||||
GetAttr(feature, "ID") ??
|
GetAttr(feature, "ID") ??
|
||||||
|
|
@ -996,35 +993,29 @@ namespace OperationControl.Controls
|
||||||
|
|
||||||
idOriginal = idOriginal?.Trim();
|
idOriginal = idOriginal?.Trim();
|
||||||
|
|
||||||
// caso esteja vazio/null
|
// Mantém o ID original quando ele existe e ainda é único.
|
||||||
if (string.IsNullOrWhiteSpace(idOriginal))
|
if (!string.IsNullOrWhiteSpace(idOriginal) &&
|
||||||
|
idsUsados.Add(idOriginal))
|
||||||
{
|
{
|
||||||
string novoId = $"AUTO_{index + 1:D4}";
|
|
||||||
idsUsados.Add(novoId);
|
|
||||||
foiGeradoAutomaticamente = true;
|
|
||||||
return novoId;
|
|
||||||
}
|
|
||||||
|
|
||||||
// se ainda não foi usado, aceita
|
|
||||||
if (!idsUsados.Contains(idOriginal))
|
|
||||||
{
|
|
||||||
idsUsados.Add(idOriginal);
|
|
||||||
return idOriginal;
|
return idOriginal;
|
||||||
}
|
}
|
||||||
|
|
||||||
// se repetiu, mantém rastreabilidade mas torna único
|
// Ausente ou repetido: procura um novo ID estritamente numérico.
|
||||||
int sufixo = 2;
|
int candidatoNumerico = Math.Max(1, index + 1);
|
||||||
string candidato;
|
|
||||||
do
|
|
||||||
{
|
|
||||||
candidato = $"{idOriginal}_{sufixo:D2}";
|
|
||||||
sufixo++;
|
|
||||||
}
|
|
||||||
while (idsUsados.Contains(candidato));
|
|
||||||
|
|
||||||
idsUsados.Add(candidato);
|
while (idsUsados.Contains(candidatoNumerico.ToString(
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture)))
|
||||||
|
{
|
||||||
|
candidatoNumerico++;
|
||||||
|
}
|
||||||
|
|
||||||
|
string novoId = candidatoNumerico.ToString(
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
idsUsados.Add(novoId);
|
||||||
foiGeradoAutomaticamente = true;
|
foiGeradoAutomaticamente = true;
|
||||||
return candidato;
|
|
||||||
|
return novoId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnCenterAreaClicked(object sender, RoutedEventArgs e)
|
private void OnCenterAreaClicked(object sender, RoutedEventArgs e)
|
||||||
|
|
@ -1057,86 +1048,424 @@ namespace OperationControl.Controls
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private Dictionary<string, string> ParseFeatureAttributes(MIFeature feature)
|
private Dictionary<string, string> ParseFeatureAttributes(MIFeature feature)
|
||||||
{
|
{
|
||||||
var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
var dict = new Dictionary<string, string>(
|
||||||
|
StringComparer.OrdinalIgnoreCase
|
||||||
|
);
|
||||||
|
|
||||||
if (feature == null)
|
if (feature == null)
|
||||||
return dict;
|
return dict;
|
||||||
|
|
||||||
var text = feature.ToStringOfKeyValuePairs();
|
string? text;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
text = feature.ToStringOfKeyValuePairs();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return dict;
|
||||||
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(text))
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
return dict;
|
return dict;
|
||||||
|
|
||||||
var lines = text.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
|
var lines = text.Split(
|
||||||
|
new[] { '\n', '\r' },
|
||||||
|
StringSplitOptions.RemoveEmptyEntries
|
||||||
|
);
|
||||||
|
|
||||||
foreach (var line in lines)
|
foreach (var line in lines)
|
||||||
{
|
{
|
||||||
var parts = line.Split(new[] { ':' }, 2); // divide em "Nome" e "valor"
|
int separatorIndex = line.IndexOf(':');
|
||||||
if (parts.Length == 2)
|
|
||||||
{
|
if (separatorIndex <= 0)
|
||||||
var key = parts[0].Trim();
|
continue;
|
||||||
var value = parts[1].Trim();
|
|
||||||
if (!string.IsNullOrEmpty(key))
|
var key = line[..separatorIndex].Trim();
|
||||||
dict[key] = value;
|
var value = line[(separatorIndex + 1)..].Trim();
|
||||||
}
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(key))
|
||||||
|
dict[key] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
return dict;
|
return dict;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string? GetAttr(MIFeature feature, string fieldName)
|
private string? GetAttr(IReadOnlyDictionary<string, string> attributes, string fieldName)
|
||||||
{
|
{
|
||||||
var dict = ParseFeatureAttributes(feature);
|
if (attributes.TryGetValue(fieldName, out var value) &&
|
||||||
return dict.TryGetValue(fieldName, out var v) ? v : null;
|
!string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return value.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
private string? GetAttr(MIFeature? feature, string fieldName)
|
||||||
|
{
|
||||||
|
if (feature == null || string.IsNullOrWhiteSpace(fieldName))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var value = feature[fieldName];
|
||||||
|
|
||||||
|
if (value == null || value is DBNull)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var text = Convert.ToString(
|
||||||
|
value,
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture
|
||||||
|
);
|
||||||
|
|
||||||
|
return string.IsNullOrWhiteSpace(text)
|
||||||
|
? null
|
||||||
|
: text.Trim();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetFirstAttr(IReadOnlyDictionary<string, string> attributes, string fallback, params string[] fieldNames)
|
||||||
|
{
|
||||||
|
foreach (var fieldName in fieldNames)
|
||||||
|
{
|
||||||
|
var value = GetAttr(attributes, fieldName);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(value))
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double GetDoubleAttr(IReadOnlyDictionary<string, string> attributes, double fallback, params string[] fieldNames)
|
||||||
|
{
|
||||||
|
foreach (var fieldName in fieldNames)
|
||||||
|
{
|
||||||
|
var value = GetAttr(attributes, fieldName);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Primeiro tenta o padrão GeoJSON/shapefile: ponto decimal.
|
||||||
|
if (double.TryParse(
|
||||||
|
value,
|
||||||
|
NumberStyles.Float | NumberStyles.AllowThousands,
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
out var invariantValue))
|
||||||
|
{
|
||||||
|
return invariantValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Depois tenta a cultura atual, caso algum mapa use vírgula.
|
||||||
|
if (double.TryParse(
|
||||||
|
value,
|
||||||
|
NumberStyles.Float | NumberStyles.AllowThousands,
|
||||||
|
CultureInfo.CurrentCulture,
|
||||||
|
out var currentValue))
|
||||||
|
{
|
||||||
|
return currentValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<List<double>> ExtrairCoordenadas(MIFeature feature)
|
||||||
|
{
|
||||||
|
var coordinates = new List<List<double>>();
|
||||||
|
|
||||||
|
if (feature == null)
|
||||||
|
return coordinates;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
dynamic geometry = ((dynamic)feature).Geometry;
|
||||||
|
|
||||||
|
if (geometry == null || geometry.CoordinateSequence == null)
|
||||||
|
return coordinates;
|
||||||
|
|
||||||
|
string? sequenceText = geometry.CoordinateSequence.ToString();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(sequenceText))
|
||||||
|
return coordinates;
|
||||||
|
|
||||||
|
sequenceText = sequenceText
|
||||||
|
.Trim()
|
||||||
|
.TrimStart('(')
|
||||||
|
.TrimEnd(')');
|
||||||
|
|
||||||
|
var pairs = sequenceText.Split(
|
||||||
|
new[] { ")," },
|
||||||
|
StringSplitOptions.RemoveEmptyEntries
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach (var pair in pairs)
|
||||||
|
{
|
||||||
|
var clean = pair
|
||||||
|
.Replace("(", "")
|
||||||
|
.Replace(")", "")
|
||||||
|
.Trim();
|
||||||
|
|
||||||
|
var parts = clean.Split(
|
||||||
|
',',
|
||||||
|
StringSplitOptions.RemoveEmptyEntries
|
||||||
|
);
|
||||||
|
|
||||||
|
// PolylineZ pode eventualmente apresentar X,Y,Z.
|
||||||
|
// Para o GeoJSON do sistema precisamos apenas de longitude/latitude.
|
||||||
|
if (parts.Length < 2)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!double.TryParse(
|
||||||
|
parts[0].Trim(),
|
||||||
|
NumberStyles.Float,
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
out var longitude))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!double.TryParse(
|
||||||
|
parts[1].Trim(),
|
||||||
|
NumberStyles.Float,
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
out var latitude))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!double.IsFinite(longitude) ||
|
||||||
|
!double.IsFinite(latitude))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
coordinates.Add(new List<double>
|
||||||
|
{
|
||||||
|
longitude,
|
||||||
|
latitude
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// A feature será descartada pelo chamador se não houver
|
||||||
|
// coordenadas suficientes.
|
||||||
|
}
|
||||||
|
|
||||||
|
return coordinates;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double CalcularComprimentoMetros(IReadOnlyList<List<double>> coordinates)
|
||||||
|
{
|
||||||
|
double comprimento = 0;
|
||||||
|
|
||||||
|
for (int i = 1; i < coordinates.Count; i++)
|
||||||
|
{
|
||||||
|
var anterior = coordinates[i - 1];
|
||||||
|
var atual = coordinates[i];
|
||||||
|
|
||||||
|
if (anterior.Count < 2 || atual.Count < 2)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
comprimento += CalcularDistanciaHaversine(
|
||||||
|
anterior[1],
|
||||||
|
anterior[0],
|
||||||
|
atual[1],
|
||||||
|
atual[0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return comprimento;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double CalcularDistanciaHaversine(double latitude1, double longitude1, double latitude2, double longitude2)
|
||||||
|
{
|
||||||
|
const double raioTerraMetros = 6371000.0;
|
||||||
|
|
||||||
|
static double ParaRadianos(double graus) => 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) * Math.Sin(dLat / 2) +
|
||||||
|
Math.Cos(lat1) *
|
||||||
|
Math.Cos(lat2) *
|
||||||
|
Math.Sin(dLon / 2) *
|
||||||
|
Math.Sin(dLon / 2);
|
||||||
|
|
||||||
|
double c = 2 * Math.Atan2(
|
||||||
|
Math.Sqrt(a),
|
||||||
|
Math.Sqrt(1 - a)
|
||||||
|
);
|
||||||
|
|
||||||
|
return raioTerraMetros * c;
|
||||||
}
|
}
|
||||||
|
|
||||||
public AgroBase.Models.MapaFeatureCollectionModel CriarDadosMapa(bool selecionadas = false)
|
public AgroBase.Models.MapaFeatureCollectionModel CriarDadosMapa(bool selecionadas = false)
|
||||||
{
|
{
|
||||||
AgroBase.Models.MapaFeatureCollectionModel _mapa = new AgroBase.Models.MapaFeatureCollectionModel() { type = "FeatureCollection", features = new List<AgroBase.Models.MapaFeatureModel>() };
|
var mapa = new AgroBase.Models.MapaFeatureCollectionModel
|
||||||
foreach (var x in RuasMapaCarregado.Where(x => selecionadas ? x.Selected : true))
|
|
||||||
{
|
{
|
||||||
string id = ((dynamic)x.Feature).Id.ToString();
|
type = "FeatureCollection",
|
||||||
string type = ((dynamic)x.Feature).Geometry.GeometryType;
|
features = new List<AgroBase.Models.MapaFeatureModel>()
|
||||||
string s = ((dynamic)x.Feature).Geometry.CoordinateSequence.ToString();
|
|
||||||
s = s.Trim().TrimStart('(').TrimEnd(')');
|
|
||||||
var pairs = s.Split("),", StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
var coordinates = new List<List<double>>();
|
|
||||||
foreach (var p in pairs)
|
|
||||||
{
|
|
||||||
var clean = p.Replace("(", "").Replace(")", "").Trim();
|
|
||||||
var parts = clean.Split(',', StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
if (parts.Length == 2 && double.TryParse(parts[0], System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out double lon) && double.TryParse(parts[1], System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out double lat))
|
|
||||||
{
|
|
||||||
coordinates.Add(new List<double> { lon, lat });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AgroBase.Models.MapaFeatureGeometryModel geometry = new AgroBase.Models.MapaFeatureGeometryModel()
|
|
||||||
{
|
|
||||||
id = GetAttr(x.Feature, "Id"),
|
|
||||||
type = type,
|
|
||||||
coordinates = coordinates
|
|
||||||
};
|
|
||||||
|
|
||||||
_mapa.features.Add(new AgroBase.Models.MapaFeatureModel()
|
|
||||||
{
|
|
||||||
type = "Feature",
|
|
||||||
geometry = geometry,
|
|
||||||
properties = new AgroBase.Models.MapaFeaturePropertiesModel()
|
|
||||||
{
|
|
||||||
Id = GetAttr(x.Feature, "Id"),
|
|
||||||
Dist1 = double.Parse(GetAttr(x.Feature, "Dist1")),
|
|
||||||
Dist2 = double.Parse(GetAttr(x.Feature, "Dist2")),
|
|
||||||
Length = double.Parse(GetAttr(x.Feature, "Length")),
|
|
||||||
Name = GetAttr(x.Feature, "Name")
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return _mapa;
|
if (RuasMapaCarregado == null)
|
||||||
|
return mapa;
|
||||||
|
|
||||||
|
var ruas = RuasMapaCarregado
|
||||||
|
.Where(x =>
|
||||||
|
x != null &&
|
||||||
|
x.Feature != null &&
|
||||||
|
(!selecionadas || x.Selected))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
int indice = 0;
|
||||||
|
|
||||||
|
foreach (var rua in ruas)
|
||||||
|
{
|
||||||
|
indice++;
|
||||||
|
|
||||||
|
var feature = rua.Feature;
|
||||||
|
var attributes = ParseFeatureAttributes(feature);
|
||||||
|
var coordinates = ExtrairCoordenadas(feature);
|
||||||
|
|
||||||
|
// Uma LineString precisa de pelo menos dois pontos.
|
||||||
|
if (coordinates.Count < 2)
|
||||||
|
{
|
||||||
|
// Se possuir seu sistema de logs, registre aqui:
|
||||||
|
// MostrarLog($"Feature {indice} ignorada: geometria inválida.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
string featureId;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
featureId =
|
||||||
|
Convert.ToString(
|
||||||
|
((dynamic)feature).Id,
|
||||||
|
CultureInfo.InvariantCulture
|
||||||
|
) ?? indice.ToString(CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
featureId = indice.ToString(CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
string geometryType;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
geometryType =
|
||||||
|
Convert.ToString(
|
||||||
|
((dynamic)feature).Geometry.GeometryType,
|
||||||
|
CultureInfo.InvariantCulture
|
||||||
|
) ?? "LineString";
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
geometryType = "LineString";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normaliza nomes que algumas bibliotecas retornam.
|
||||||
|
if (geometryType.Contains(
|
||||||
|
"LineString",
|
||||||
|
StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
geometryType = "LineString";
|
||||||
|
}
|
||||||
|
else if (geometryType.Contains(
|
||||||
|
"PolyLine",
|
||||||
|
StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
geometryType = "LineString";
|
||||||
|
}
|
||||||
|
|
||||||
|
string id = GetFirstAttr(
|
||||||
|
attributes,
|
||||||
|
featureId,
|
||||||
|
"Id",
|
||||||
|
"ID",
|
||||||
|
"FID",
|
||||||
|
"OBJECTID"
|
||||||
|
);
|
||||||
|
|
||||||
|
string name = GetFirstAttr(
|
||||||
|
attributes,
|
||||||
|
$"Linha {indice}",
|
||||||
|
"Name",
|
||||||
|
"Nome",
|
||||||
|
"LAYER"
|
||||||
|
);
|
||||||
|
|
||||||
|
double calculatedLength =
|
||||||
|
CalcularComprimentoMetros(coordinates);
|
||||||
|
|
||||||
|
double length = GetDoubleAttr(
|
||||||
|
attributes,
|
||||||
|
calculatedLength,
|
||||||
|
"Length",
|
||||||
|
"Comprimento",
|
||||||
|
"Shape_Leng",
|
||||||
|
"SHAPE_LEN"
|
||||||
|
);
|
||||||
|
|
||||||
|
double dist1 = GetDoubleAttr(
|
||||||
|
attributes,
|
||||||
|
0.0,
|
||||||
|
"Dist1"
|
||||||
|
);
|
||||||
|
|
||||||
|
double dist2 = GetDoubleAttr(
|
||||||
|
attributes,
|
||||||
|
0.0,
|
||||||
|
"Dist2"
|
||||||
|
);
|
||||||
|
|
||||||
|
var geometry =
|
||||||
|
new AgroBase.Models.MapaFeatureGeometryModel
|
||||||
|
{
|
||||||
|
id = id,
|
||||||
|
type = geometryType,
|
||||||
|
coordinates = coordinates
|
||||||
|
};
|
||||||
|
|
||||||
|
mapa.features.Add(
|
||||||
|
new AgroBase.Models.MapaFeatureModel
|
||||||
|
{
|
||||||
|
type = "Feature",
|
||||||
|
geometry = geometry,
|
||||||
|
properties =
|
||||||
|
new AgroBase.Models.MapaFeaturePropertiesModel
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
Dist1 = dist1,
|
||||||
|
Dist2 = dist2,
|
||||||
|
Length = length,
|
||||||
|
Name = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapa;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public void CarregarDadosMapa(AgroBase.Models.MapaFeatureCollectionModel Mapa, List<string> RuasSelecionadas, List<double[]> pontosSelecionados)
|
public void CarregarDadosMapa(AgroBase.Models.MapaFeatureCollectionModel Mapa, List<string> RuasSelecionadas, List<double[]> pontosSelecionados)
|
||||||
{
|
{
|
||||||
if (RuasMapaCarregado.Any())
|
if (RuasMapaCarregado.Any())
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ namespace OperationControl.Controls
|
||||||
|
|
||||||
private void BicoCtrl_OnBicoClicked(int index)
|
private void BicoCtrl_OnBicoClicked(int index)
|
||||||
{
|
{
|
||||||
if (!(VariaveisControleOperacao.RoverEmFoco?.Controle?.PulverizadorAutomatico ?? false))
|
if ((VariaveisControleOperacao.RoverEmFoco?.Controle?.PulverizadorAutomatico ?? false))
|
||||||
{
|
{
|
||||||
System.Windows.MessageBox.Show("Para acionar os bicos manualmente, é necessário que o Pulverizador Automático esteja desligado", "Pulverizador automático", MessageBoxButton.OK, MessageBoxImage.Information);
|
System.Windows.MessageBox.Show("Para acionar os bicos manualmente, é necessário que o Pulverizador Automático esteja desligado", "Pulverizador automático", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -11,17 +11,35 @@ namespace OperationControl.ViewModels.Views.Operacao.Monitoramento
|
||||||
public class PulverizadorViewModel : INotifyPropertyChanged
|
public class PulverizadorViewModel : INotifyPropertyChanged
|
||||||
{
|
{
|
||||||
public ObservableCollection<FrameTipoItem> CameraErvasFrameTipos { get; set; }
|
public ObservableCollection<FrameTipoItem> CameraErvasFrameTipos { get; set; }
|
||||||
private FrameTipoItem _cameraErvasFrameTipoSelecionado;
|
private FrameTipoItem? _cameraErvasFrameTipoSelecionado;
|
||||||
public FrameTipoItem CameraErvasFrameTipoSelecionado
|
|
||||||
|
public FrameTipoItem? CameraErvasFrameTipoSelecionado
|
||||||
{
|
{
|
||||||
get => _cameraErvasFrameTipoSelecionado;
|
get => _cameraErvasFrameTipoSelecionado;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (_cameraErvasFrameTipoSelecionado != value)
|
if (value == null ||
|
||||||
|
ReferenceEquals(_cameraErvasFrameTipoSelecionado, value))
|
||||||
{
|
{
|
||||||
_cameraErvasFrameTipoSelecionado = value;
|
return;
|
||||||
OnPropertyChanged();
|
}
|
||||||
VariaveisControleOperacao.EnviarComandoTransmissaoVideo(AgroBase.Models.Enums.T_Code.Cam, true, value.Valor);
|
|
||||||
|
_cameraErvasFrameTipoSelecionado = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
VariaveisControleOperacao.EnviarComandoTransmissaoVideo(
|
||||||
|
T_Code.Cam,
|
||||||
|
true,
|
||||||
|
value.Valor
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Models.Variaveis.MostrarLog(
|
||||||
|
$"Erro ao alterar o tipo de frame da câmera: {ex.Message}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -37,8 +55,12 @@ namespace OperationControl.ViewModels.Views.Operacao.Monitoramento
|
||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
CameraErvasFrameTipos = new ObservableCollection<FrameTipoItem>(itens);
|
CameraErvasFrameTipos =
|
||||||
CameraErvasFrameTipoSelecionado = CameraErvasFrameTipos.FirstOrDefault();
|
new ObservableCollection<FrameTipoItem>(itens);
|
||||||
|
|
||||||
|
// Inicializa sem enviar comando durante a construção da tela.
|
||||||
|
_cameraErvasFrameTipoSelecionado =
|
||||||
|
CameraErvasFrameTipos.FirstOrDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObservableCollection<string> EventosPulverizador { get; set; } = new();
|
public ObservableCollection<string> EventosPulverizador { get; set; } = new();
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,7 @@ class BombaPressurizadora_v2 : public ComponenteCAN {
|
||||||
Ela segue uma rampa.
|
Ela segue uma rampa.
|
||||||
*/
|
*/
|
||||||
double TaxaSubidaPctPorSegundo = 180.0;
|
double TaxaSubidaPctPorSegundo = 180.0;
|
||||||
double TaxaDescidaPctPorSegundo = 240.0;
|
double TaxaDescidaPctPorSegundo = 120.0;
|
||||||
unsigned long IntervaloControleMs = 50;
|
unsigned long IntervaloControleMs = 50;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
@ -189,12 +189,29 @@ class BombaPressurizadora_v2 : public ComponenteCAN {
|
||||||
double TaxaBoostFluxoDescidaPctPorSegundo = 240.0;
|
double TaxaBoostFluxoDescidaPctPorSegundo = 240.0;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Protecao relativa ao SP. Ela nao substitui o corte absoluto de 120 psi.
|
Alívio suave de sobrepressão.
|
||||||
A primeira margem cancela kick/boost; a segunda pausa a bomba com histerese.
|
|
||||||
|
Não desliga a bomba. Cancela kick/boost e aplica uma redução
|
||||||
|
proporcional na potência enquanto a pressão estiver acima do SP.
|
||||||
|
|
||||||
|
O corte físico completo continua sendo responsabilidade exclusiva
|
||||||
|
de PressaoCorteSegurancaPsi.
|
||||||
*/
|
*/
|
||||||
double MargemCancelarAssistenciasPsi = 1.5;
|
double MargemCancelarAssistenciasPsi = 1.5;
|
||||||
double MargemPausaSobrepressaoPsi = 8.0;
|
|
||||||
double MargemRearmeSobrepressaoPsi = 1.0;
|
// Apenas indica que o alívio está efetivamente atuando.
|
||||||
|
double MargemAtivarAlivioSobrepressaoPsi = 1.2;
|
||||||
|
|
||||||
|
// Histerese da flag de diagnóstico.
|
||||||
|
double MargemMarcarSobrepressaoPsi = 3.0;
|
||||||
|
double MargemLimparSobrepressaoPsi = 1.0;
|
||||||
|
|
||||||
|
// Redução adicional de potência para cada psi acima da margem.
|
||||||
|
double GanhoAlivioSobrepressaoPctPorPsi = 2.0;
|
||||||
|
|
||||||
|
// Limita a intervenção adicional. O PI continua trabalhando junto.
|
||||||
|
double ReducaoMaximaSobrepressaoPct = 35.0;
|
||||||
|
|
||||||
bool ProtecaoSobrepressaoRelativaAtiva = false;
|
bool ProtecaoSobrepressaoRelativaAtiva = false;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
@ -1108,21 +1125,48 @@ class BombaPressurizadora_v2 : public ComponenteCAN {
|
||||||
}
|
}
|
||||||
|
|
||||||
double ObterPotenciaMinimaRegulacao(uint8_t idx) {
|
double ObterPotenciaMinimaRegulacao(uint8_t idx) {
|
||||||
idx = LimitarIndiceBicos(idx);
|
(void)idx;
|
||||||
|
|
||||||
double minimo = PotenciaMinAtiva;
|
/*
|
||||||
|
Durante a regulação, permite que o PID desça até o piso elétrico
|
||||||
|
configurado pelo C#.
|
||||||
|
|
||||||
if (idx > 0) {
|
Se PotenciaMinPID = 40%, a bomba continuará ligada, mas poderá
|
||||||
minimo = MaiorDouble(minimo, PotenciaMinimaComBicosPorQtd[idx]);
|
reduzir abaixo dos pisos específicos de 5 ou 6 bicos.
|
||||||
}
|
*/
|
||||||
|
double minimo = MaiorDouble(
|
||||||
|
PotenciaMinAtiva,
|
||||||
|
PotenciaMinPID
|
||||||
|
);
|
||||||
|
|
||||||
return constrain(minimo, 0.0, PotenciaMaxPID);
|
return constrain(
|
||||||
|
minimo,
|
||||||
|
0.0,
|
||||||
|
PotenciaMaxPID
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
double ObterPotenciaMinimaPartida(uint8_t idx) {
|
double ObterPotenciaMinimaPartida(uint8_t idx) {
|
||||||
|
idx = LimitarIndiceBicos(idx);
|
||||||
|
|
||||||
double minimo = ObterPotenciaMinimaRegulacao(idx);
|
double minimo = ObterPotenciaMinimaRegulacao(idx);
|
||||||
minimo = MaiorDouble(minimo, PotenciaMinPID);
|
|
||||||
return constrain(minimo, 0.0, PotenciaMaxPID);
|
/*
|
||||||
|
O piso por quantidade de bicos continua sendo usado na partida.
|
||||||
|
Portanto, preservamos a força necessária para iniciar o fluxo.
|
||||||
|
*/
|
||||||
|
if (idx > 0) {
|
||||||
|
minimo = MaiorDouble(
|
||||||
|
minimo,
|
||||||
|
PotenciaMinimaComBicosPorQtd[idx]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return constrain(
|
||||||
|
minimo,
|
||||||
|
0.0,
|
||||||
|
PotenciaMaxPID
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
double ObterPressaoMaximaMapeada(uint8_t idx) {
|
double ObterPressaoMaximaMapeada(uint8_t idx) {
|
||||||
|
|
@ -1273,53 +1317,71 @@ class BombaPressurizadora_v2 : public ComponenteCAN {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TratarSobrepressaoRelativa(double pressaoSP) {
|
double AtualizarAlivioSobrepressao(double pressaoSP) {
|
||||||
if (!SensorPressaoValidoAtual || pressaoSP <= 0.0) {
|
if (!SensorPressaoValidoAtual || pressaoSP <= 0.0) {
|
||||||
ProtecaoSobrepressaoRelativaAtiva = false;
|
ProtecaoSobrepressaoRelativaAtiva = false;
|
||||||
return false;
|
bombaDesligadaPorPressao = false;
|
||||||
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Usa a maior pressão entre a leitura rápida e a filtrada.
|
||||||
|
|
||||||
|
A leitura rápida permite reagir antes que o filtro acompanhe
|
||||||
|
completamente a subida da pressão.
|
||||||
|
*/
|
||||||
double pressaoProtecao = MaiorDouble(
|
double pressaoProtecao = MaiorDouble(
|
||||||
PressaoBrutaControleAtual,
|
PressaoBrutaControleAtual,
|
||||||
PressaoFiltrada
|
PressaoFiltrada
|
||||||
);
|
);
|
||||||
|
|
||||||
bool atingiuRegiaoDeCancelamento =
|
double excessoPressao = pressaoProtecao - pressaoSP;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Quando a pressão se aproxima do alvo, kick e boost deixam de
|
||||||
|
competir com o PID.
|
||||||
|
*/
|
||||||
|
bool chegouProximoDoSP =
|
||||||
pressaoProtecao >=
|
pressaoProtecao >=
|
||||||
(pressaoSP - MargemCancelarAssistenciasPsi);
|
(pressaoSP - MargemCancelarAssistenciasPsi);
|
||||||
|
|
||||||
if (atingiuRegiaoDeCancelamento) {
|
if (chegouProximoDoSP) {
|
||||||
ResetarKick();
|
ResetarKick();
|
||||||
ResetarSupervisorFluxo();
|
ResetarSupervisorFluxo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Flag com histerese exclusivamente para diagnóstico/health.
|
||||||
|
Ela não desliga a bomba.
|
||||||
|
*/
|
||||||
if (
|
if (
|
||||||
pressaoProtecao >=
|
excessoPressao >=
|
||||||
(pressaoSP + MargemPausaSobrepressaoPsi)
|
MargemMarcarSobrepressaoPsi
|
||||||
) {
|
) {
|
||||||
ProtecaoSobrepressaoRelativaAtiva = true;
|
ProtecaoSobrepressaoRelativaAtiva = true;
|
||||||
}
|
}
|
||||||
|
else if (
|
||||||
if (
|
|
||||||
ProtecaoSobrepressaoRelativaAtiva &&
|
ProtecaoSobrepressaoRelativaAtiva &&
|
||||||
pressaoProtecao <=
|
excessoPressao <=
|
||||||
(pressaoSP - MargemRearmeSobrepressaoPsi)
|
MargemLimparSobrepressaoPsi
|
||||||
) {
|
) {
|
||||||
ProtecaoSobrepressaoRelativaAtiva = false;
|
ProtecaoSobrepressaoRelativaAtiva = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ProtecaoSobrepressaoRelativaAtiva) {
|
/*
|
||||||
bombaDesligadaPorPressao = false;
|
Somente o corte absoluto pode marcar a bomba como desligada
|
||||||
return false;
|
por pressão.
|
||||||
|
*/
|
||||||
|
bombaDesligadaPorPressao = false;
|
||||||
|
|
||||||
|
if (
|
||||||
|
excessoPressao <=
|
||||||
|
MargemAtivarAlivioSobrepressaoPsi
|
||||||
|
) {
|
||||||
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
AplicarSaida(false, 0);
|
return excessoPressao;
|
||||||
ResetarIntegralPressao();
|
|
||||||
ResetarKick();
|
|
||||||
ResetarSupervisorFluxo();
|
|
||||||
bombaDesligadaPorPressao = true;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ControlarOnOffLegado(double pressaoSP, double potenciaOn, double histeresePsi) {
|
void ControlarOnOffLegado(double pressaoSP, double potenciaOn, double histeresePsi) {
|
||||||
|
|
@ -1381,10 +1443,7 @@ class BombaPressurizadora_v2 : public ComponenteCAN {
|
||||||
PressaoSPForaDoMapaAtual = SPForaDoMapa(idx, spControle);
|
PressaoSPForaDoMapaAtual = SPForaDoMapa(idx, spControle);
|
||||||
PressaoSPLimitadoPorCapacidade = false;
|
PressaoSPLimitadoPorCapacidade = false;
|
||||||
|
|
||||||
if (TratarSobrepressaoRelativa(spControle)) {
|
double excessoSobrepressao = AtualizarAlivioSobrepressao(spControle);
|
||||||
_ultimaPressaoFiltrada = PressaoFiltrada;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
double potenciaBase = CalcularPotenciaFeedForward(idx, spControle);
|
double potenciaBase = CalcularPotenciaFeedForward(idx, spControle);
|
||||||
PotenciaFeedForwardAtual = potenciaBase;
|
PotenciaFeedForwardAtual = potenciaBase;
|
||||||
|
|
@ -1472,9 +1531,58 @@ class BombaPressurizadora_v2 : public ComponenteCAN {
|
||||||
IniciarKick(idx);
|
IniciarKick(idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
potenciaDesejada = AplicarKickInicial(potenciaDesejada, idx);
|
potenciaDesejada = AplicarKickInicial(
|
||||||
|
potenciaDesejada,
|
||||||
|
idx
|
||||||
|
);
|
||||||
|
|
||||||
double potenciaComRampa = AplicarRampaPercentual(potenciaDesejada, dt);
|
/*
|
||||||
|
Se houver sobrepressão, acrescenta uma correção negativa proporcional.
|
||||||
|
|
||||||
|
Isso trabalha junto com o erro negativo do PI. Não substitui o PI e
|
||||||
|
não zera a bomba.
|
||||||
|
*/
|
||||||
|
if (
|
||||||
|
excessoSobrepressao >
|
||||||
|
MargemAtivarAlivioSobrepressaoPsi
|
||||||
|
) {
|
||||||
|
double excessoEfetivo =
|
||||||
|
excessoSobrepressao -
|
||||||
|
MargemAtivarAlivioSobrepressaoPsi;
|
||||||
|
|
||||||
|
double reducaoSobrepressao =
|
||||||
|
excessoEfetivo *
|
||||||
|
GanhoAlivioSobrepressaoPctPorPsi;
|
||||||
|
|
||||||
|
reducaoSobrepressao = constrain(
|
||||||
|
reducaoSobrepressao,
|
||||||
|
0.0,
|
||||||
|
ReducaoMaximaSobrepressaoPct
|
||||||
|
);
|
||||||
|
|
||||||
|
potenciaDesejada -= reducaoSobrepressao;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Mantém a bomba operando no piso configurado, atualmente 40%.
|
||||||
|
*/
|
||||||
|
potenciaDesejada = MaiorDouble(
|
||||||
|
potenciaDesejada,
|
||||||
|
ObterPotenciaMinimaRegulacao(idx)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
potenciaDesejada = constrain(
|
||||||
|
potenciaDesejada,
|
||||||
|
ObterPotenciaMinimaRegulacao(idx),
|
||||||
|
PotenciaMaxPID
|
||||||
|
);
|
||||||
|
|
||||||
|
double potenciaComRampa =
|
||||||
|
AplicarRampaPercentual(
|
||||||
|
potenciaDesejada,
|
||||||
|
dt
|
||||||
|
);
|
||||||
|
|
||||||
AplicarPotenciaPercentual(potenciaComRampa);
|
AplicarPotenciaPercentual(potenciaComRampa);
|
||||||
|
|
||||||
_ultimoErroPressao = erro;
|
_ultimoErroPressao = erro;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue