Compare commits
3 Commits
ff06844686
...
608c9b5e85
| Author | SHA1 | Date |
|---|---|---|
|
|
608c9b5e85 | |
|
|
cd3125da98 | |
|
|
f1e37c1dae |
|
|
@ -4425,14 +4425,48 @@ namespace AgroBase.Models
|
|||
private void IniciarRetornoBaseCore(GPSModel baseGps, List<GPSModel> waypointsManual)
|
||||
{
|
||||
bool valido = false;
|
||||
|
||||
List<GPSModel> pontosRetorno = null;
|
||||
|
||||
List<GPSModel> waypointsManualPersistir =
|
||||
null;
|
||||
|
||||
|
||||
// =========================
|
||||
// MODO MANUAL (operador)
|
||||
// MODO MANUAL
|
||||
// =========================
|
||||
if (waypointsManual != null && waypointsManual.Count > 0)
|
||||
if (
|
||||
waypointsManual != null &&
|
||||
waypointsManual.Count > 0
|
||||
)
|
||||
{
|
||||
pontosRetorno = ValidarEPrepararWaypointsRetorno(waypointsManual, "manual");
|
||||
pontosRetorno =
|
||||
ValidarEPrepararWaypointsRetorno(
|
||||
waypointsManual,
|
||||
"manual"
|
||||
);
|
||||
|
||||
|
||||
/*
|
||||
* Esta é a rota que representa a intenção
|
||||
* do operador e deve voltar para a base.
|
||||
*/
|
||||
waypointsManualPersistir =
|
||||
pontosRetorno
|
||||
.Select(
|
||||
x =>
|
||||
new GPSModel
|
||||
{
|
||||
Latitude =
|
||||
x.Latitude,
|
||||
|
||||
Longitude =
|
||||
x.Longitude
|
||||
}
|
||||
)
|
||||
.ToList();
|
||||
|
||||
|
||||
valido = true;
|
||||
}
|
||||
else
|
||||
|
|
@ -4441,19 +4475,49 @@ namespace AgroBase.Models
|
|||
// MODO AUTOMÁTICO
|
||||
// =========================
|
||||
if (PontoGpsValido(baseGps))
|
||||
(valido, pontosRetorno) = GerarTrajetoriaRetornoAutomatico(baseGps);
|
||||
{
|
||||
(
|
||||
valido,
|
||||
pontosRetorno
|
||||
) =
|
||||
GerarTrajetoriaRetornoAutomatico(
|
||||
baseGps
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!valido || pontosRetorno == null || pontosRetorno.Count < 1)
|
||||
|
||||
if (
|
||||
!valido ||
|
||||
pontosRetorno == null ||
|
||||
pontosRetorno.Count < 1
|
||||
)
|
||||
{
|
||||
throw new InvalidOperationException("Trajetoria de retorno invalida ou rota segura indisponivel.");
|
||||
}
|
||||
else
|
||||
{
|
||||
pontosRetorno = ValidarEPrepararWaypointsRetorno(pontosRetorno, "calculado");
|
||||
AplicarTrajetoriaFixaRetorno(pontosRetorno, "RetornoBase", waypointsManual?.Any() ?? false);
|
||||
AtualizarDadosOperacaoParaRetorno();
|
||||
throw new InvalidOperationException(
|
||||
"Trajetoria de retorno invalida " +
|
||||
"ou rota segura indisponivel."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
pontosRetorno =
|
||||
ValidarEPrepararWaypointsRetorno(
|
||||
pontosRetorno,
|
||||
"calculado"
|
||||
);
|
||||
|
||||
|
||||
AplicarTrajetoriaFixaRetorno(
|
||||
pontosRetorno,
|
||||
"RetornoBase",
|
||||
waypointsManualPersistir?.Any()
|
||||
?? false
|
||||
);
|
||||
|
||||
|
||||
AtualizarDadosOperacaoParaRetorno(
|
||||
waypointsManualPersistir
|
||||
);
|
||||
}
|
||||
|
||||
private List<GPSModel> ValidarEPrepararWaypointsRetorno(List<GPSModel> pontos, string origem)
|
||||
|
|
@ -4623,7 +4687,7 @@ namespace AgroBase.Models
|
|||
return new List<GPSModel>(pontos);
|
||||
}
|
||||
|
||||
private void AtualizarDadosOperacaoParaRetorno()
|
||||
private void AtualizarDadosOperacaoParaRetorno(List<GPSModel> waypointsManual)
|
||||
{
|
||||
var op = Variaveis.OperacaoEmAndamento;
|
||||
|
||||
|
|
@ -4640,6 +4704,26 @@ namespace AgroBase.Models
|
|||
|
||||
op.Parametros = new_op.Parametros;
|
||||
|
||||
/*
|
||||
* Mantém somente a rota MANUAL recebida do operador.
|
||||
*
|
||||
* Em retorno automático, fica null.
|
||||
*
|
||||
* Contrato:
|
||||
* [latitude, longitude]
|
||||
*/
|
||||
op.Parametros.PontosRetorno =
|
||||
waypointsManual?
|
||||
.Select(
|
||||
x =>
|
||||
new double[]
|
||||
{
|
||||
x.Latitude,
|
||||
x.Longitude
|
||||
}
|
||||
)
|
||||
.ToList();
|
||||
|
||||
if (!op.Sensoriamento.Operacao.OperacaoIniciada)
|
||||
{
|
||||
op.Sensoriamento.Operacao.OperacaoIniciada = true;
|
||||
|
|
|
|||
|
|
@ -11,9 +11,6 @@ from .radiometric_controller import RadiometricController
|
|||
|
||||
PHYSICAL_CHANNEL_NAMES = ("R", "G", "B", "RE", "NIR")
|
||||
PHYSICAL_CHANNEL_COUNT = len(PHYSICAL_CHANNEL_NAMES)
|
||||
OAK_FCC3_CLIENT_VERSION = "production_v1_2026_08_24"
|
||||
PRODUCT_SCHEMA = "multispec_module_params_v3"
|
||||
ASSEMBLY_SCHEMA = "multispec_module_params_assembly_v1"
|
||||
|
||||
|
||||
class OakFcc3Client:
|
||||
|
|
@ -44,110 +41,83 @@ class OakFcc3Client:
|
|||
capture_mode="AUTO",
|
||||
raw_policy="allow_single",
|
||||
module_calibration_json=None,
|
||||
sync_mode="best",
|
||||
sync_tolerance_ms=25.0,
|
||||
mx_id=None,
|
||||
imu_modo="rotation_vector",
|
||||
imu_freq_hz=200,
|
||||
evaluate_quality=True,
|
||||
require_product_contract=False,
|
||||
|
||||
hardware_sync_enabled=None,
|
||||
frame_sync_master=None,
|
||||
sync_mode=None,
|
||||
sync_tolerance_ms=None,
|
||||
buffer_size=None,
|
||||
|
||||
**kwargs,
|
||||
):
|
||||
# width/height/bayer recebidos do caller ficam apenas como fallback legado.
|
||||
# Em module_params de produção, hardware real + MP são a autoridade.
|
||||
self.legacy_width = int(width)
|
||||
self.legacy_height = int(height)
|
||||
self.legacy_bayer = str(bayer or "BGGR").upper()
|
||||
|
||||
self.fps = float(fps)
|
||||
self.frame_type = str(frame_type).upper()
|
||||
self.output_dtype = str(output_dtype).lower()
|
||||
self.capture_mode = str(capture_mode).upper()
|
||||
self.raw_policy = str(raw_policy).lower()
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.bayer = bayer
|
||||
self.fps = fps
|
||||
self.frame_type = frame_type
|
||||
self.output_dtype = output_dtype
|
||||
self.capture_mode = capture_mode
|
||||
self.raw_policy = raw_policy
|
||||
self.module_calibration_json = module_calibration_json
|
||||
self.require_product_contract = bool(require_product_contract)
|
||||
|
||||
self.module_params = self._load_module_params(
|
||||
module_calibration_json,
|
||||
required=self.require_product_contract,
|
||||
)
|
||||
self.product_contract = self._is_product_module_params(self.module_params)
|
||||
|
||||
if self.require_product_contract and not self.product_contract:
|
||||
raise RuntimeError(
|
||||
"Contrato de produção obrigatório, mas module_params não foi "
|
||||
"gerado pelo assembler oficial."
|
||||
)
|
||||
|
||||
self.module_params = self._load_module_params(module_calibration_json)
|
||||
self.fusion_config = self.module_params.get("fusion_config", {}) or {}
|
||||
self.camera_hardware = self._resolve_camera_hardware()
|
||||
self.sensor_size_by_role = self._resolve_sensor_size_by_role()
|
||||
|
||||
rgb_size = self.sensor_size_by_role.get(
|
||||
"rgb",
|
||||
[self.legacy_width, self.legacy_height],
|
||||
)
|
||||
self.rgb_native_width = int(rgb_size[0])
|
||||
self.rgb_native_height = int(rgb_size[1])
|
||||
|
||||
# Mantidos por compatibilidade, mas agora significam RGB de referência.
|
||||
self.width = self.rgb_native_width
|
||||
self.height = self.rgb_native_height
|
||||
|
||||
self.bayer = self._resolve_bayer_pattern()
|
||||
self.default_target_size = self._resolve_default_target_size()
|
||||
|
||||
self.imu_modo = str(imu_modo).strip().lower()
|
||||
self.imu_freq_hz = int(imu_freq_hz)
|
||||
|
||||
# Auditoria radiométrica completa do Raw5.
|
||||
#
|
||||
# True mantém o comportamento histórico e é útil para captura científica,
|
||||
# normalize/auditoria e ferramentas offline.
|
||||
#
|
||||
# No runtime em tempo real deve ficar False: evaluate_frame_quality()
|
||||
# calcula estatísticas/percentis pesados e não faz parte da montagem
|
||||
# necessária para a inferência.
|
||||
self.evaluate_quality = bool(evaluate_quality)
|
||||
|
||||
self.mx_id = str(mx_id) if mx_id else None
|
||||
|
||||
# Validação antecipada do modo produto. O Manager também repete estes
|
||||
# checks antes de abrir o hardware, de propósito.
|
||||
self._validate_static_product_contract()
|
||||
|
||||
self.svc = OakFcc3Service(
|
||||
timeout=10,
|
||||
fps=self.fps,
|
||||
width=self.width,
|
||||
height=self.height,
|
||||
frame_type=self.frame_type,
|
||||
output_dtype=self.output_dtype,
|
||||
capture_mode=self.capture_mode,
|
||||
raw_policy=self.raw_policy,
|
||||
sync_mode=sync_mode,
|
||||
sync_tolerance_ms=sync_tolerance_ms,
|
||||
fps=fps,
|
||||
width=width,
|
||||
height=height,
|
||||
frame_type=frame_type,
|
||||
output_dtype=output_dtype,
|
||||
capture_mode=capture_mode,
|
||||
raw_policy=raw_policy,
|
||||
mx_id=self.mx_id,
|
||||
module_calibration_json=module_calibration_json,
|
||||
module_params=self.module_params,
|
||||
require_product_contract=self.require_product_contract,
|
||||
|
||||
imu_modo=self.imu_modo,
|
||||
imu_freq_hz=self.imu_freq_hz,
|
||||
|
||||
hardware_sync_enabled=hardware_sync_enabled,
|
||||
frame_sync_master=frame_sync_master,
|
||||
sync_mode=sync_mode,
|
||||
sync_tolerance_ms=sync_tolerance_ms,
|
||||
buffer_size=buffer_size,
|
||||
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
self.applied_camera_controls = {}
|
||||
self.radiometric_controller = None
|
||||
self.radiometric_controller_enabled = False
|
||||
|
||||
self.core = RawProcessorCore(
|
||||
sensor_width=self.rgb_native_width,
|
||||
sensor_height=self.rgb_native_height,
|
||||
bayer_pattern=self.bayer,
|
||||
sensor_width=width,
|
||||
sensor_height=height,
|
||||
bayer_pattern=bayer,
|
||||
calibration_json_path=module_calibration_json,
|
||||
)
|
||||
|
||||
# Preview é apenas visual, porém também precisa usar o Bayer/raster
|
||||
# reais do RGB para não mentir sobre a AR0234.
|
||||
self.preview = RawProcessorPreview(
|
||||
sensor_width=self.rgb_native_width,
|
||||
sensor_height=self.rgb_native_height,
|
||||
bayer_pattern=self.bayer,
|
||||
sensor_width=width,
|
||||
sensor_height=height,
|
||||
bayer_pattern=bayer,
|
||||
)
|
||||
self._preview_cache = {
|
||||
(self.rgb_native_width, self.rgb_native_height, self.bayer): self.preview,
|
||||
}
|
||||
|
||||
def __enter__(self):
|
||||
self.start()
|
||||
|
|
@ -156,247 +126,19 @@ class OakFcc3Client:
|
|||
def __exit__(self, exc_type, exc, tb):
|
||||
self.stop()
|
||||
|
||||
def _load_module_params(self, path, required=False):
|
||||
if not path:
|
||||
if required:
|
||||
raise FileNotFoundError(
|
||||
"module_params obrigatório no contrato de produção."
|
||||
)
|
||||
return {}
|
||||
|
||||
if not os.path.isfile(path):
|
||||
if required:
|
||||
raise FileNotFoundError(
|
||||
f"module_params não encontrado: {path}"
|
||||
)
|
||||
def _load_module_params(self, path):
|
||||
if not path or not os.path.isfile(path):
|
||||
return {}
|
||||
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
if not isinstance(data, dict):
|
||||
raise RuntimeError(
|
||||
f"module_params root deve ser dict/object: {path}"
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def _is_product_module_params(module_params):
|
||||
mp = module_params or {}
|
||||
assembly = mp.get("assembly_metadata", {}) or {}
|
||||
return bool(
|
||||
mp.get("schema") == PRODUCT_SCHEMA
|
||||
and assembly.get("schema") == ASSEMBLY_SCHEMA
|
||||
and isinstance(mp.get("camera_hardware"), dict)
|
||||
and isinstance(mp.get("sensor_size_by_role"), dict)
|
||||
and isinstance(mp.get("calibration_provenance"), dict)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _normalize_size(value, label):
|
||||
if not (isinstance(value, (list, tuple)) and len(value) == 2):
|
||||
raise RuntimeError(f"{label} deve ser [W,H], recebido={value!r}")
|
||||
|
||||
w = int(value[0])
|
||||
h = int(value[1])
|
||||
if w <= 0 or h <= 0:
|
||||
raise RuntimeError(f"{label} inválido: {value!r}")
|
||||
return [w, h]
|
||||
|
||||
def _resolve_camera_hardware(self):
|
||||
raw = self.module_params.get("camera_hardware", {}) or {}
|
||||
|
||||
if not self.product_contract:
|
||||
return {}
|
||||
|
||||
out = {}
|
||||
expected = {
|
||||
"rgb": ("CAM_A", ("OV9782", "AR0234")),
|
||||
"re": ("CAM_B", ("OV9282",)),
|
||||
"nir": ("CAM_C", ("OV9282",)),
|
||||
}
|
||||
|
||||
for role, (expected_socket, allowed_sensors) in expected.items():
|
||||
item = raw.get(role)
|
||||
if not isinstance(item, dict):
|
||||
raise RuntimeError(f"camera_hardware sem role={role}")
|
||||
|
||||
socket = str(item.get("socket") or item.get("socket_name") or "").upper()
|
||||
sensor = str(item.get("sensor") or item.get("sensor_name") or "").upper()
|
||||
size = self._normalize_size(item.get("size"), f"camera_hardware.{role}.size")
|
||||
|
||||
if socket != expected_socket:
|
||||
raise RuntimeError(
|
||||
f"camera_hardware.{role}.socket={socket!r}, esperado={expected_socket!r}"
|
||||
)
|
||||
|
||||
if sensor not in allowed_sensors:
|
||||
raise RuntimeError(
|
||||
f"camera_hardware.{role}.sensor={sensor!r}, permitidos={allowed_sensors}"
|
||||
)
|
||||
|
||||
out[role] = {
|
||||
"role": role,
|
||||
"socket": socket,
|
||||
"sensor": sensor,
|
||||
"size": size,
|
||||
}
|
||||
|
||||
return out
|
||||
|
||||
def _resolve_sensor_size_by_role(self):
|
||||
if not self.product_contract:
|
||||
return {
|
||||
"rgb": [self.legacy_width, self.legacy_height],
|
||||
"re": [self.legacy_width, self.legacy_height],
|
||||
"nir": [self.legacy_width, self.legacy_height],
|
||||
}
|
||||
|
||||
raw = self.module_params.get("sensor_size_by_role", {}) or {}
|
||||
out = {}
|
||||
|
||||
for role in ("rgb", "re", "nir"):
|
||||
size = self._normalize_size(
|
||||
raw.get(role),
|
||||
f"sensor_size_by_role.{role}",
|
||||
)
|
||||
expected = self.camera_hardware[role]["size"]
|
||||
if size != expected:
|
||||
raise RuntimeError(
|
||||
f"sensor_size_by_role.{role}={size} != camera_hardware={expected}"
|
||||
)
|
||||
out[role] = size
|
||||
|
||||
return out
|
||||
|
||||
def _resolve_bayer_pattern(self):
|
||||
value = self.module_params.get("bayer_pattern") if self.product_contract else None
|
||||
bayer = str(value or self.legacy_bayer).upper()
|
||||
|
||||
if bayer not in ("RGGB", "BGGR", "GRBG", "GBRG"):
|
||||
raise RuntimeError(f"bayer_pattern inválido: {bayer!r}")
|
||||
|
||||
return bayer
|
||||
|
||||
def _resolve_default_target_size(self):
|
||||
value = (self.fusion_config or {}).get("target_size")
|
||||
if value is None:
|
||||
return None
|
||||
return self._normalize_size(value, "fusion_config.target_size")
|
||||
|
||||
def _resolve_target_size(self, target_size):
|
||||
value = target_size if target_size is not None else self.default_target_size
|
||||
if value is None:
|
||||
return None
|
||||
size = self._normalize_size(value, "target_size")
|
||||
|
||||
if self.product_contract and self.default_target_size is not None:
|
||||
if size != self.default_target_size:
|
||||
raise RuntimeError(
|
||||
"target_size solicitado diverge do module_params homologado: "
|
||||
f"requested={size}, calibrated_runtime={self.default_target_size}"
|
||||
)
|
||||
|
||||
return size
|
||||
|
||||
def _validate_static_product_contract(self):
|
||||
if not self.product_contract:
|
||||
return
|
||||
|
||||
if self.frame_type != "RAW_BRUTO":
|
||||
raise RuntimeError(
|
||||
"OakFcc3Client de produção aceita somente frame_type='RAW_BRUTO'."
|
||||
)
|
||||
|
||||
if self.raw_policy != "require_triple":
|
||||
raise RuntimeError(
|
||||
"OakFcc3Client de produção exige raw_policy='require_triple'."
|
||||
)
|
||||
|
||||
if self.capture_mode not in ("TRIPLE", "AUTO"):
|
||||
raise RuntimeError(
|
||||
"OakFcc3Client de produção exige capture_mode TRIPLE/AUTO."
|
||||
)
|
||||
|
||||
expected_mx = (
|
||||
(self.module_params.get("calibration_provenance", {}) or {})
|
||||
.get("device_mx_id")
|
||||
)
|
||||
if expected_mx and self.mx_id and str(expected_mx) != self.mx_id:
|
||||
raise RuntimeError(
|
||||
"MX ID solicitado diverge da calibração homologada: "
|
||||
f"requested={self.mx_id}, calibrated={expected_mx}"
|
||||
)
|
||||
|
||||
def _sync_contract_from_manager_status(self, status):
|
||||
if not isinstance(status, dict):
|
||||
return
|
||||
|
||||
if self.product_contract and not bool(status.get("product_contract", False)):
|
||||
raise RuntimeError(
|
||||
"Client carregou module_params produto, mas Manager não reconheceu o contrato."
|
||||
)
|
||||
|
||||
sizes = status.get("sensor_size_by_role")
|
||||
if isinstance(sizes, dict):
|
||||
normalized = {
|
||||
role: self._normalize_size(sizes.get(role), f"manager.sensor_size_by_role.{role}")
|
||||
for role in ("rgb", "re", "nir")
|
||||
}
|
||||
if self.product_contract and normalized != self.sensor_size_by_role:
|
||||
raise RuntimeError(
|
||||
"Manager e Client discordam sobre sensor_size_by_role: "
|
||||
f"manager={normalized}, client={self.sensor_size_by_role}"
|
||||
)
|
||||
self.sensor_size_by_role = normalized
|
||||
|
||||
bayer = status.get("bayer_pattern")
|
||||
if bayer:
|
||||
bayer = str(bayer).upper()
|
||||
if self.product_contract and bayer != self.bayer:
|
||||
raise RuntimeError(
|
||||
f"Manager Bayer={bayer} != Client/module_params={self.bayer}"
|
||||
)
|
||||
|
||||
def get_contract(self):
|
||||
return {
|
||||
"client_version": OAK_FCC3_CLIENT_VERSION,
|
||||
"product_contract": bool(self.product_contract),
|
||||
"require_product_contract": bool(self.require_product_contract),
|
||||
"sensor_size_by_role": {
|
||||
role: list(size)
|
||||
for role, size in self.sensor_size_by_role.items()
|
||||
},
|
||||
"camera_hardware": json.loads(json.dumps(self.camera_hardware)),
|
||||
"bayer_pattern": self.bayer,
|
||||
"default_target_size": (
|
||||
None if self.default_target_size is None
|
||||
else list(self.default_target_size)
|
||||
),
|
||||
"evaluate_quality": bool(self.evaluate_quality),
|
||||
"radiometric_controller_enabled": bool(self.radiometric_controller_enabled),
|
||||
}
|
||||
return json.load(f)
|
||||
|
||||
def apply_module_camera_settings(self):
|
||||
camera_settings = self.module_params.get("camera_settings", {}) or {}
|
||||
|
||||
if self.product_contract:
|
||||
missing = [
|
||||
role
|
||||
for role in ("rgb", "re", "nir")
|
||||
if not isinstance(camera_settings.get(role), dict)
|
||||
]
|
||||
if missing:
|
||||
raise RuntimeError(
|
||||
f"module_params produto sem camera_settings para: {missing}"
|
||||
)
|
||||
|
||||
applied = {}
|
||||
roles = ("rgb", "re", "nir") if self.product_contract else tuple(camera_settings.keys())
|
||||
|
||||
for role in roles:
|
||||
settings = camera_settings.get(role)
|
||||
for role, settings in camera_settings.items():
|
||||
if not isinstance(settings, dict):
|
||||
continue
|
||||
|
||||
|
|
@ -415,43 +157,17 @@ class OakFcc3Client:
|
|||
}
|
||||
|
||||
self.applied_camera_controls = applied
|
||||
|
||||
if self.product_contract:
|
||||
failed = {
|
||||
role: value
|
||||
for role, value in applied.items()
|
||||
if not bool((value or {}).get("ok", False))
|
||||
}
|
||||
if failed:
|
||||
raise RuntimeError(
|
||||
f"Falha reaplicando camera_settings homologado: {failed}"
|
||||
)
|
||||
|
||||
return applied
|
||||
|
||||
def enable_radiometric_controller(self):
|
||||
cfg = self.module_params.get("radiometric_config", {}) or {}
|
||||
enabled = bool(cfg.get("enabled", False))
|
||||
|
||||
# No produto atual este controller de exposição em campo é OFF.
|
||||
# Não instanciamos um segundo piloto para ficar parado dentro do loop.
|
||||
if not enabled:
|
||||
self.radiometric_controller = None
|
||||
self.radiometric_controller_enabled = False
|
||||
return None
|
||||
|
||||
self.radiometric_controller = RadiometricController(
|
||||
client=self,
|
||||
config_json_path=self.module_calibration_json,
|
||||
)
|
||||
|
||||
self.radiometric_controller.sync_from_camera_controls(
|
||||
self.applied_camera_controls
|
||||
)
|
||||
|
||||
self.radiometric_controller.sync_from_camera_controls(self.applied_camera_controls)
|
||||
self.radiometric_controller.sync_from_actual_camera_controls()
|
||||
self.radiometric_controller_enabled = bool(
|
||||
getattr(self.radiometric_controller, "enabled", True)
|
||||
)
|
||||
|
||||
return self.radiometric_controller
|
||||
|
||||
def update_radiometry(self, decoded, meta=None):
|
||||
|
|
@ -491,45 +207,17 @@ class OakFcc3Client:
|
|||
)
|
||||
|
||||
try:
|
||||
status = self.svc.get_status()
|
||||
self.mx_id = str(status.get("mx_id") or self.mx_id or "") or None
|
||||
self._sync_contract_from_manager_status(status)
|
||||
self.mx_id = self.svc.manager.mx_id
|
||||
except Exception:
|
||||
# Se a validação de contrato falhar, fecha hardware antes de propagar.
|
||||
try:
|
||||
self.svc.disconnect()
|
||||
except Exception:
|
||||
pass
|
||||
raise
|
||||
pass
|
||||
|
||||
# O Manager de produção já aplicou estes controles via initialControl.
|
||||
# Reaplicamos após start como confirmação operacional e para manter
|
||||
# compatibilidade com Managers legados durante a migração.
|
||||
applied = self.apply_module_camera_settings()
|
||||
|
||||
self.enable_radiometric_controller()
|
||||
|
||||
if print_debug:
|
||||
print("[OAK CLIENT] START:", resp)
|
||||
print("[OAK CLIENT] CONTRACT:", self.get_contract())
|
||||
print("[OAK CLIENT] APPLIED CAMERA SETTINGS:", applied)
|
||||
|
||||
if isinstance(resp, dict):
|
||||
resp = dict(resp)
|
||||
resp["client_version"] = OAK_FCC3_CLIENT_VERSION
|
||||
resp["product_contract"] = bool(self.product_contract)
|
||||
resp["sensor_size_by_role"] = {
|
||||
role: list(size)
|
||||
for role, size in self.sensor_size_by_role.items()
|
||||
}
|
||||
resp["bayer_pattern"] = self.bayer
|
||||
resp["default_target_size"] = (
|
||||
None if self.default_target_size is None
|
||||
else list(self.default_target_size)
|
||||
)
|
||||
resp["radiometric_controller_enabled"] = bool(
|
||||
self.radiometric_controller_enabled
|
||||
)
|
||||
self.enable_radiometric_controller()
|
||||
|
||||
return resp
|
||||
|
||||
|
|
@ -541,29 +229,7 @@ class OakFcc3Client:
|
|||
return self.svc.get_device_metrics()
|
||||
|
||||
def get_status(self):
|
||||
status = self.svc.get_status()
|
||||
if not isinstance(status, dict):
|
||||
status = {}
|
||||
else:
|
||||
status = dict(status)
|
||||
|
||||
status.update({
|
||||
"client_version": OAK_FCC3_CLIENT_VERSION,
|
||||
"client_product_contract": bool(self.product_contract),
|
||||
"client_require_product_contract": bool(self.require_product_contract),
|
||||
"client_sensor_size_by_role": {
|
||||
role: list(size)
|
||||
for role, size in self.sensor_size_by_role.items()
|
||||
},
|
||||
"client_bayer_pattern": self.bayer,
|
||||
"client_default_target_size": (
|
||||
None if self.default_target_size is None
|
||||
else list(self.default_target_size)
|
||||
),
|
||||
"evaluate_quality": bool(self.evaluate_quality),
|
||||
"radiometric_controller_enabled": bool(self.radiometric_controller_enabled),
|
||||
})
|
||||
return status
|
||||
return self.svc.get_status()
|
||||
|
||||
def get_next_raw_frame(self, timeout=1.0):
|
||||
return self.svc.capture_frame(timeout=timeout)
|
||||
|
|
@ -578,12 +244,6 @@ class OakFcc3Client:
|
|||
frame_type = str(raw_meta.get("frame_type", self.frame_type)).upper()
|
||||
meta = dict(raw_meta)
|
||||
|
||||
if self.product_contract and frame_type != "RAW_BRUTO":
|
||||
raise RuntimeError(
|
||||
"OakFcc3Client produto recebeu frame_type não canônico: "
|
||||
f"{frame_type!r}. Esperado='RAW_BRUTO'."
|
||||
)
|
||||
|
||||
if frame_type == "RAW_BRUTO":
|
||||
decoded = self.decode_stream_cameras(raw_frame, raw_meta)
|
||||
|
||||
|
|
@ -680,25 +340,6 @@ class OakFcc3Client:
|
|||
|
||||
def build_infer_tensor(self, frame, meta, channels_expected, target_size=None):
|
||||
channels_expected = self._validate_physical_channel_count(channels_expected)
|
||||
target_size = self._resolve_target_size(target_size)
|
||||
|
||||
frame_type = str(
|
||||
(meta or {}).get("frame_type", self.frame_type)
|
||||
if isinstance(meta, dict)
|
||||
else self.frame_type
|
||||
).upper()
|
||||
|
||||
# No runtime quente evitamos o quality audit pesado. Decodificamos e
|
||||
# usamos exatamente o mesmo caminho do CameraMultispectral.
|
||||
if not self.evaluate_quality and frame_type == "RAW_BRUTO":
|
||||
decoded = self.core.decode_stream_cameras(frame, meta)
|
||||
return self.build_infer_tensor_from_decoded(
|
||||
decoded=decoded,
|
||||
meta=meta,
|
||||
channels_expected=channels_expected,
|
||||
target_size=target_size,
|
||||
)
|
||||
|
||||
return self.core.build_infer_tensor_from_stream(
|
||||
frame,
|
||||
meta,
|
||||
|
|
@ -714,49 +355,41 @@ class OakFcc3Client:
|
|||
target_size=None,
|
||||
evaluate_quality=None,
|
||||
):
|
||||
"""
|
||||
Monta o Raw5 físico a partir das câmeras já decodificadas.
|
||||
|
||||
evaluate_quality:
|
||||
- None -> usa self.evaluate_quality
|
||||
- True -> executa evaluate_frame_quality() e atualiza
|
||||
core.last_frame_quality_result
|
||||
- False -> não executa a auditoria pesada e limpa
|
||||
core.last_frame_quality_result
|
||||
|
||||
A flag altera somente a auditoria de qualidade. Não altera decode,
|
||||
radiometria, flat-field, homografia, crop/resize ou patch normalization.
|
||||
"""
|
||||
channels_expected = self._validate_physical_channel_count(channels_expected)
|
||||
target_size = self._resolve_target_size(target_size)
|
||||
|
||||
if evaluate_quality is None:
|
||||
evaluate_quality = self.evaluate_quality
|
||||
evaluate_quality = bool(evaluate_quality)
|
||||
|
||||
# Core novo faz a geometria source->target em uma única etapa.
|
||||
# NÃO redimensionar novamente depois da fusão.
|
||||
tensor = self.core.fuse_multispec_cameras(
|
||||
decoded,
|
||||
meta,
|
||||
channels_expected,
|
||||
target_size=target_size,
|
||||
)
|
||||
|
||||
patch_cfg = getattr(
|
||||
self.core,
|
||||
"patch_normalization_config",
|
||||
{},
|
||||
) or {}
|
||||
tensor = self.core.fuse_multispec_cameras(decoded, meta, channels_expected)
|
||||
tensor = self.core.resize_tensor_chw(tensor, target_size=target_size)
|
||||
|
||||
# Mantém paridade com build_infer_tensor_from_stream: se a calibração
|
||||
# habilitar patch normalization, ela também vale no caminho decoded.
|
||||
patch_cfg = getattr(self.core, "patch_normalization_config", {}) or {}
|
||||
if bool(patch_cfg.get("enabled", False)):
|
||||
if self.product_contract:
|
||||
raise RuntimeError(
|
||||
"patch_normalization não é permitido no contrato produto."
|
||||
)
|
||||
tensor = self.core.apply_patch_normalization_to_tensor(tensor)
|
||||
|
||||
if evaluate_quality:
|
||||
self.core.last_frame_quality_result = self.core.evaluate_frame_quality(tensor)
|
||||
else:
|
||||
# Evita deixar resultado antigo no objeto e evita percentis no hot path.
|
||||
self.core.last_frame_quality_result = {
|
||||
"status": "skipped",
|
||||
"usable_for_training": None,
|
||||
"reason": "disabled_by_oak_fcc3_client",
|
||||
"client_version": OAK_FCC3_CLIENT_VERSION,
|
||||
}
|
||||
# Evita deixar um resultado antigo parecer referente ao frame atual.
|
||||
self.core.last_frame_quality_result = None
|
||||
|
||||
return np.ascontiguousarray(
|
||||
tensor.astype(np.float32, copy=False)
|
||||
)
|
||||
return tensor
|
||||
|
||||
def decode_stream_cameras(self, frame, meta):
|
||||
if str(meta.get("frame_type", self.frame_type)).upper() == "PREVIEW":
|
||||
|
|
@ -872,14 +505,10 @@ class OakFcc3Client:
|
|||
arr = arr[:, :, 0]
|
||||
|
||||
if bit_depth == 10 and arr.ndim == 2:
|
||||
role_size = self.sensor_size_by_role.get(
|
||||
str(role).lower(),
|
||||
[self.rgb_native_width, self.rgb_native_height],
|
||||
)
|
||||
raw16 = self.core.unpack_raw10_packed(
|
||||
arr,
|
||||
sensor_width=int(info.get("width", role_size[0])),
|
||||
sensor_height=int(info.get("height", role_size[1])),
|
||||
sensor_width=int(info.get("width", self.width)),
|
||||
sensor_height=int(info.get("height", self.height)),
|
||||
)
|
||||
|
||||
if role == "rgb":
|
||||
|
|
@ -941,7 +570,7 @@ class OakFcc3Client:
|
|||
or cam_meta.get("bayer")
|
||||
or stream_meta.get("bayer_pattern")
|
||||
or bayer_pattern
|
||||
or self.bayer
|
||||
or "RGGB"
|
||||
)
|
||||
bayer = str(bayer).upper()
|
||||
|
||||
|
|
@ -973,17 +602,19 @@ class OakFcc3Client:
|
|||
real_w = int(cam_meta.get("width", sensor_width))
|
||||
real_h = int(cam_meta.get("height", sensor_height))
|
||||
|
||||
cache_key = (real_w, real_h, bayer)
|
||||
preview = self._preview_cache.get(cache_key)
|
||||
if preview is None:
|
||||
preview = RawProcessorPreview(
|
||||
sensor_width=real_w,
|
||||
sensor_height=real_h,
|
||||
bayer_pattern=bayer,
|
||||
)
|
||||
self._preview_cache[cache_key] = preview
|
||||
core = RawProcessorCore(
|
||||
sensor_width=real_w,
|
||||
sensor_height=real_h,
|
||||
bayer_pattern=bayer,
|
||||
)
|
||||
|
||||
raw16 = self.core.unpack_raw10_packed(
|
||||
preview = RawProcessorPreview(
|
||||
sensor_width=real_w,
|
||||
sensor_height=real_h,
|
||||
bayer_pattern=bayer,
|
||||
)
|
||||
|
||||
raw16 = core.unpack_raw10_packed(
|
||||
packed,
|
||||
sensor_width=real_w,
|
||||
sensor_height=real_h,
|
||||
|
|
@ -1050,11 +681,6 @@ class OakFcc3Client:
|
|||
|
||||
|
||||
def decode_oak_aligned_multispec(self, frame, meta):
|
||||
if self.product_contract:
|
||||
raise RuntimeError(
|
||||
"MULTISPEC alinhado pela OAK é legado e não faz parte do contrato produto."
|
||||
)
|
||||
|
||||
"""
|
||||
Decodifica frames já alinhados pela OAK.
|
||||
|
||||
|
|
@ -1103,11 +729,6 @@ class OakFcc3Client:
|
|||
return decoded
|
||||
|
||||
def build_multispec_tensor_from_oak_aligned(self, decoded, meta=None):
|
||||
if self.product_contract:
|
||||
raise RuntimeError(
|
||||
"Tensor MULTISPEC pré-alinhado pela OAK é legado no contrato produto."
|
||||
)
|
||||
|
||||
"""
|
||||
Monta CHW [R,G,B,RE,NIR] sem reaplicar homografia.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -12,60 +12,27 @@ import numpy as np
|
|||
from itertools import product
|
||||
|
||||
|
||||
OAK_FCC3_MANAGER_VERSION = "production_v1_2026_08_24"
|
||||
|
||||
|
||||
class OakFcc3Manager:
|
||||
"""
|
||||
Hardware manager de produção para OAK-FFC-3 multiespectral.
|
||||
Manager OAK-FFC-3 com dois fluxos principais:
|
||||
|
||||
Contrato oficial:
|
||||
CAM_A = RGB = OV9782 1280x800 OU AR0234 1920x1200
|
||||
CAM_B = RE = OV9282 1280x800
|
||||
CAM_C = NIR = OV9282 1280x800
|
||||
1) RAW_BRUTO
|
||||
- Mantém o comportamento antigo.
|
||||
- CAM_A/CAM_B/CAM_C enviam RAW10 packed direto para o PC.
|
||||
- O PC faz decode, flat/radiometric, homografia/fusão/crop/resize.
|
||||
|
||||
Caminho oficial de produto:
|
||||
RAW_BRUTO -> RAW10 packed nativo por câmera -> RawProcessorCore.
|
||||
|
||||
O Manager não calibra imagem. Ele abre/valida hardware, aplica a política
|
||||
inicial da câmera, sincroniza RAW e publica metadata fiel por câmera.
|
||||
|
||||
PREVIEW/MULTISPEC permanecem apenas para compatibilidade legada.
|
||||
Um module_params produzido pelo assembler oficial exige RAW_BRUTO.
|
||||
2) MULTISPEC
|
||||
- Câmeras sempre em 800p nativo.
|
||||
- OAK aplica homografia/crop/resize via ImageManip.
|
||||
- PC recebe frames já alinhados:
|
||||
CAM_A/rgb -> BGR uint8
|
||||
CAM_B/re -> GRAY uint8
|
||||
CAM_C/nir -> GRAY uint8
|
||||
- O Client deve montar o tensor sem reaplicar homografia.
|
||||
"""
|
||||
|
||||
PRODUCT_SCHEMA = "multispec_module_params_v3"
|
||||
ASSEMBLY_SCHEMA = "multispec_module_params_assembly_v1"
|
||||
|
||||
LEGACY_SENSOR_W = 1280
|
||||
LEGACY_SENSOR_H = 800
|
||||
|
||||
PRODUCT_TOPOLOGY = {
|
||||
"rgb": {"socket": "CAM_A", "allowed_sensors": ("OV9782", "AR0234")},
|
||||
"re": {"socket": "CAM_B", "allowed_sensors": ("OV9282",)},
|
||||
"nir": {"socket": "CAM_C", "allowed_sensors": ("OV9282",)},
|
||||
}
|
||||
|
||||
SENSOR_PROFILES = {
|
||||
"OV9782": {
|
||||
"kind": "color",
|
||||
"resolution_name": "THE_800_P",
|
||||
"width": 1280,
|
||||
"height": 800,
|
||||
},
|
||||
"AR0234": {
|
||||
"kind": "color",
|
||||
"resolution_name": "THE_1200_P",
|
||||
"width": 1920,
|
||||
"height": 1200,
|
||||
},
|
||||
"OV9282": {
|
||||
"kind": "mono",
|
||||
"resolution_name": "THE_800_P",
|
||||
"width": 1280,
|
||||
"height": 800,
|
||||
},
|
||||
}
|
||||
SENSOR_W = 1280
|
||||
SENSOR_H = 800
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -77,75 +44,41 @@ class OakFcc3Manager:
|
|||
capture_mode="AUTO",
|
||||
raw_policy="allow_single",
|
||||
roles=None,
|
||||
sync_mode="best",
|
||||
hardware_sync_enabled=True,
|
||||
frame_sync_master="CAM_A",
|
||||
sync_tolerance_ms=12.0,
|
||||
buffer_size=8,
|
||||
sync_mode=None,
|
||||
hardware_sync_enabled=None,
|
||||
frame_sync_master=None,
|
||||
sync_tolerance_ms=None,
|
||||
buffer_size=None,
|
||||
only_camera=None,
|
||||
mx_id=None,
|
||||
module_calibration_json=None,
|
||||
module_params=None,
|
||||
require_product_contract=False,
|
||||
imu_modo="rotation_vector",
|
||||
imu_freq_hz=200,
|
||||
):
|
||||
self.hardware_sync_enabled = bool(hardware_sync_enabled)
|
||||
self.frame_sync_master = str(frame_sync_master).upper()
|
||||
self.fps = float(fps)
|
||||
self.fps = fps
|
||||
|
||||
# width/height são apenas saída LEGADA. RAW_BRUTO usa resolução nativa.
|
||||
# Para compatibilidade, mantemos width/height.
|
||||
# No RAW_BRUTO isso não muda o sensor, pois usamos 800p fixo.
|
||||
# No MULTISPEC isso representa a saída final alinhada da OAK.
|
||||
self.width = int(width)
|
||||
self.height = int(height)
|
||||
self.size = (self.width, self.height)
|
||||
|
||||
self.sensor_width = self.SENSOR_W
|
||||
self.sensor_height = self.SENSOR_H
|
||||
|
||||
self.frame_type = str(frame_type).upper()
|
||||
self.output_dtype = output_dtype
|
||||
self.capture_mode = str(capture_mode).upper()
|
||||
self.raw_policy = str(raw_policy).lower()
|
||||
self.capture_mode = capture_mode
|
||||
self.raw_policy = raw_policy
|
||||
self.only_camera = only_camera
|
||||
|
||||
self.module_calibration_json = module_calibration_json
|
||||
self.module_params = (
|
||||
copy.deepcopy(module_params)
|
||||
if isinstance(module_params, dict)
|
||||
else self._load_module_params(module_calibration_json)
|
||||
)
|
||||
self.fusion_config = (self.module_params or {}).get("fusion_config", {}) or {}
|
||||
self.require_product_contract = bool(require_product_contract)
|
||||
|
||||
self.product_contract = self._is_product_module_params()
|
||||
self.expected_device_mx_id = self._expected_device_mx_id()
|
||||
|
||||
if self.product_contract:
|
||||
self.roles = {
|
||||
contract["socket"]: role
|
||||
for role, contract in self.PRODUCT_TOPOLOGY.items()
|
||||
}
|
||||
else:
|
||||
self.roles = roles or {
|
||||
"CAM_A": "rgb",
|
||||
"CAM_B": "re",
|
||||
"CAM_C": "nir",
|
||||
}
|
||||
|
||||
self.expected_camera_hardware = self._resolve_expected_camera_hardware()
|
||||
self.sensor_size_by_role = self._resolve_sensor_size_by_role()
|
||||
|
||||
rgb_size = self.sensor_size_by_role.get(
|
||||
"rgb",
|
||||
[self.LEGACY_SENSOR_W, self.LEGACY_SENSOR_H],
|
||||
)
|
||||
self.sensor_width = int(rgb_size[0])
|
||||
self.sensor_height = int(rgb_size[1])
|
||||
|
||||
self.bayer_pattern = str(
|
||||
(self.module_params or {}).get("bayer_pattern", "BGGR")
|
||||
).upper()
|
||||
|
||||
self.sync_mode = str(sync_mode).lower()
|
||||
self.sync_tolerance_ms = float(sync_tolerance_ms)
|
||||
self.buffer_size = int(buffer_size)
|
||||
self.roles = roles or {
|
||||
"CAM_A": "rgb",
|
||||
"CAM_B": "re",
|
||||
"CAM_C": "nir",
|
||||
}
|
||||
|
||||
self.mx_id = str(mx_id) if mx_id else None
|
||||
self.dev_info = None
|
||||
|
|
@ -157,12 +90,14 @@ class OakFcc3Manager:
|
|||
|
||||
self.imu_modo = self._validate_imu_modo(imu_modo)
|
||||
self.imu_freq_hz = int(imu_freq_hz)
|
||||
|
||||
if self.imu_freq_hz <= 0:
|
||||
raise ValueError(
|
||||
f"imu_freq_hz deve ser maior que zero: {self.imu_freq_hz}"
|
||||
)
|
||||
|
||||
self.imu_sensor_type = None
|
||||
|
||||
self.has_imu_pipeline = False
|
||||
self.tem_imu = False
|
||||
self.q_imu = None
|
||||
|
|
@ -170,18 +105,32 @@ class OakFcc3Manager:
|
|||
self.running = False
|
||||
self.frame_id = 0
|
||||
|
||||
# Serializa start/stop e leituras nativas de telemetria.
|
||||
# Evita getChipTemperature/getUsbSpeed concorrendo com device.close().
|
||||
self._device_lock = threading.RLock()
|
||||
self.control_queues = {}
|
||||
self._last_raw_dims = {}
|
||||
self.aligned_geometry = None
|
||||
|
||||
# Startup Profile entra ANTES do pipeline existir.
|
||||
self.camera_controls = {
|
||||
cam_id: self._default_controls_for_role(role)
|
||||
for cam_id, role in self.roles.items()
|
||||
}
|
||||
self._hydrate_camera_controls_from_module_params()
|
||||
self._validate_static_product_contract()
|
||||
self._last_raw_dims = {}
|
||||
|
||||
self.module_calibration_json = module_calibration_json
|
||||
self.module_params = module_params if isinstance(module_params, dict) else self._load_module_params(module_calibration_json)
|
||||
self.fusion_config = (self.module_params or {}).get("fusion_config", {}) or {}
|
||||
self.aligned_geometry = None
|
||||
|
||||
sync_cfg = (self.module_params.get("capture_synchronization", {}) if isinstance(self.module_params, dict) else {})
|
||||
if hardware_sync_enabled is None: hardware_sync_enabled = sync_cfg.get("hardware_sync_enabled", False)
|
||||
if frame_sync_master is None: frame_sync_master = sync_cfg.get("frame_sync_master", "CAM_A")
|
||||
if sync_mode is None: sync_mode = sync_cfg.get("software_sync_mode", "best")
|
||||
if sync_tolerance_ms is None: sync_tolerance_ms = sync_cfg.get("sync_tolerance_ms", 12.0)
|
||||
if buffer_size is None: buffer_size = sync_cfg.get("buffer_size", 8)
|
||||
self.hardware_sync_enabled = bool(hardware_sync_enabled)
|
||||
self.frame_sync_master = str(frame_sync_master)
|
||||
self.sync_mode = str(sync_mode)
|
||||
self.sync_tolerance_ms = float(sync_tolerance_ms)
|
||||
self.buffer_size = int(buffer_size)
|
||||
|
||||
self.async_capture_enabled = True
|
||||
self.async_capture_mode = "latest" # latest | queue
|
||||
|
|
@ -228,327 +177,11 @@ class OakFcc3Manager:
|
|||
# ============================================================
|
||||
|
||||
def _load_module_params(self, path):
|
||||
if not path:
|
||||
if not path or not os.path.isfile(path):
|
||||
return {}
|
||||
|
||||
if not os.path.isfile(path):
|
||||
raise FileNotFoundError(
|
||||
f"module_params não encontrado: {path}"
|
||||
)
|
||||
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
if not isinstance(data, dict):
|
||||
raise RuntimeError(
|
||||
f"module_params root deve ser dict/object: {path}"
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
def _is_product_module_params(self):
|
||||
mp = self.module_params or {}
|
||||
assembly = mp.get("assembly_metadata", {}) or {}
|
||||
|
||||
return bool(
|
||||
mp.get("schema") == self.PRODUCT_SCHEMA
|
||||
and assembly.get("schema") == self.ASSEMBLY_SCHEMA
|
||||
and isinstance(mp.get("camera_hardware"), dict)
|
||||
and isinstance(mp.get("sensor_size_by_role"), dict)
|
||||
and isinstance(mp.get("calibration_provenance"), dict)
|
||||
)
|
||||
|
||||
def _expected_device_mx_id(self):
|
||||
prov = (self.module_params or {}).get("calibration_provenance", {}) or {}
|
||||
value = prov.get("device_mx_id")
|
||||
return str(value) if value else None
|
||||
|
||||
def _normalize_size(self, value, *, label):
|
||||
if not (isinstance(value, (list, tuple)) and len(value) == 2):
|
||||
raise RuntimeError(f"{label} deve ser [W,H], recebido={value!r}")
|
||||
|
||||
w = int(value[0])
|
||||
h = int(value[1])
|
||||
|
||||
if w <= 0 or h <= 0:
|
||||
raise RuntimeError(f"{label} inválido: {value!r}")
|
||||
|
||||
return [w, h]
|
||||
|
||||
def _resolve_expected_camera_hardware(self):
|
||||
if not self.product_contract:
|
||||
return {}
|
||||
|
||||
raw = (self.module_params or {}).get("camera_hardware", {}) or {}
|
||||
out = {}
|
||||
|
||||
for role, contract in self.PRODUCT_TOPOLOGY.items():
|
||||
item = raw.get(role)
|
||||
|
||||
if not isinstance(item, dict):
|
||||
raise RuntimeError(
|
||||
f"module_params.camera_hardware sem role={role}"
|
||||
)
|
||||
|
||||
socket = str(item.get("socket") or "").upper()
|
||||
sensor = str(item.get("sensor") or "").upper()
|
||||
size = self._normalize_size(
|
||||
item.get("size"),
|
||||
label=f"camera_hardware.{role}.size",
|
||||
)
|
||||
|
||||
if socket != contract["socket"]:
|
||||
raise RuntimeError(
|
||||
f"camera_hardware.{role}.socket={socket!r}, "
|
||||
f"esperado={contract['socket']!r}"
|
||||
)
|
||||
|
||||
if sensor not in contract["allowed_sensors"]:
|
||||
raise RuntimeError(
|
||||
f"camera_hardware.{role}.sensor={sensor!r}, "
|
||||
f"permitidos={contract['allowed_sensors']}"
|
||||
)
|
||||
|
||||
profile = self.SENSOR_PROFILES.get(sensor)
|
||||
|
||||
if profile is None:
|
||||
raise RuntimeError(
|
||||
f"Sensor sem profile de runtime: {sensor}"
|
||||
)
|
||||
|
||||
native = [int(profile["width"]), int(profile["height"])]
|
||||
|
||||
if size != native:
|
||||
raise RuntimeError(
|
||||
f"camera_hardware.{role}.size={size} "
|
||||
f"não corresponde ao nativo de {sensor}: {native}"
|
||||
)
|
||||
|
||||
out[role] = {
|
||||
"role": role,
|
||||
"socket": socket,
|
||||
"sensor": sensor,
|
||||
"size": size,
|
||||
"kind": profile["kind"],
|
||||
"resolution_name": profile["resolution_name"],
|
||||
}
|
||||
|
||||
return out
|
||||
|
||||
def _resolve_sensor_size_by_role(self):
|
||||
if self.product_contract:
|
||||
raw = (self.module_params or {}).get("sensor_size_by_role", {}) or {}
|
||||
out = {}
|
||||
|
||||
for role in ("rgb", "re", "nir"):
|
||||
size = self._normalize_size(
|
||||
raw.get(role),
|
||||
label=f"sensor_size_by_role.{role}",
|
||||
)
|
||||
expected = self.expected_camera_hardware[role]["size"]
|
||||
|
||||
if size != expected:
|
||||
raise RuntimeError(
|
||||
f"sensor_size_by_role.{role}={size} != "
|
||||
f"camera_hardware={expected}"
|
||||
)
|
||||
|
||||
out[role] = size
|
||||
|
||||
return out
|
||||
|
||||
return {
|
||||
"rgb": [self.LEGACY_SENSOR_W, self.LEGACY_SENSOR_H],
|
||||
"re": [self.LEGACY_SENSOR_W, self.LEGACY_SENSOR_H],
|
||||
"nir": [self.LEGACY_SENSOR_W, self.LEGACY_SENSOR_H],
|
||||
}
|
||||
|
||||
def _validate_static_product_contract(self):
|
||||
if self.require_product_contract and not self.product_contract:
|
||||
raise RuntimeError(
|
||||
"Contrato de produção obrigatório, mas o module_params "
|
||||
"não foi gerado pelo assembler oficial."
|
||||
)
|
||||
|
||||
if not self.product_contract:
|
||||
return
|
||||
|
||||
if self.frame_type != "RAW_BRUTO":
|
||||
raise RuntimeError(
|
||||
"module_params de produção aceita somente frame_type=RAW_BRUTO. "
|
||||
f"Recebido={self.frame_type!r}"
|
||||
)
|
||||
|
||||
if self.capture_mode not in ("TRIPLE", "AUTO"):
|
||||
raise RuntimeError(
|
||||
"Produto multiespectral exige capture_mode TRIPLE "
|
||||
f"(ou AUTO com require_triple). Recebido={self.capture_mode!r}"
|
||||
)
|
||||
|
||||
if self.raw_policy != "require_triple":
|
||||
raise RuntimeError(
|
||||
"module_params de produção exige raw_policy='require_triple'. "
|
||||
f"Recebido={self.raw_policy!r}"
|
||||
)
|
||||
|
||||
if self.only_camera is not None:
|
||||
raise RuntimeError(
|
||||
"only_camera não é permitido no contrato de produção."
|
||||
)
|
||||
|
||||
if self.bayer_pattern not in ("RGGB", "BGGR", "GRBG", "GBRG"):
|
||||
raise RuntimeError(
|
||||
f"bayer_pattern inválido no module_params: {self.bayer_pattern!r}"
|
||||
)
|
||||
|
||||
if (
|
||||
self.mx_id is not None
|
||||
and self.expected_device_mx_id is not None
|
||||
and self.mx_id != self.expected_device_mx_id
|
||||
):
|
||||
raise RuntimeError(
|
||||
"MX ID solicitado diverge da calibração homologada: "
|
||||
f"requested={self.mx_id}, calibrated={self.expected_device_mx_id}"
|
||||
)
|
||||
|
||||
def _role_to_cam_id(self, role):
|
||||
role = str(role).lower()
|
||||
|
||||
for cam_id, mapped_role in self.roles.items():
|
||||
if str(mapped_role).lower() == role:
|
||||
return cam_id
|
||||
|
||||
return None
|
||||
|
||||
def _hydrate_camera_controls_from_module_params(self):
|
||||
settings = (self.module_params or {}).get("camera_settings", {}) or {}
|
||||
|
||||
if not isinstance(settings, dict):
|
||||
if self.product_contract:
|
||||
raise RuntimeError(
|
||||
"module_params de produção sem camera_settings."
|
||||
)
|
||||
return
|
||||
|
||||
for role, cfg in settings.items():
|
||||
if not isinstance(cfg, dict):
|
||||
continue
|
||||
|
||||
cam_id = self._role_to_cam_id(role)
|
||||
|
||||
if cam_id is None:
|
||||
continue
|
||||
|
||||
state = self.camera_controls.setdefault(
|
||||
cam_id,
|
||||
self._default_controls_for_role(role),
|
||||
)
|
||||
|
||||
for key in (
|
||||
"ae_enable",
|
||||
"awb_enable",
|
||||
"exposure_time_us",
|
||||
"analogue_gain",
|
||||
"colour_gains",
|
||||
):
|
||||
if key in cfg:
|
||||
state[key] = copy.deepcopy(cfg[key])
|
||||
|
||||
if self.product_contract:
|
||||
for role in ("rgb", "re", "nir"):
|
||||
cam_id = self._role_to_cam_id(role)
|
||||
|
||||
if cam_id is None or cam_id not in self.camera_controls:
|
||||
raise RuntimeError(
|
||||
f"camera_settings não resolveu role={role}"
|
||||
)
|
||||
|
||||
def _sensor_profile(self, sensor_name, role=None):
|
||||
sensor = str(sensor_name or "").upper()
|
||||
profile = self.SENSOR_PROFILES.get(sensor)
|
||||
|
||||
if profile is None:
|
||||
raise RuntimeError(
|
||||
f"Sensor não suportado pelo runtime: {sensor!r}"
|
||||
)
|
||||
|
||||
if role is not None:
|
||||
role = str(role).lower()
|
||||
expected_kind = "color" if role == "rgb" else "mono"
|
||||
|
||||
if profile["kind"] != expected_kind:
|
||||
raise RuntimeError(
|
||||
f"Sensor {sensor} é {profile['kind']}, "
|
||||
f"mas role={role} exige {expected_kind}."
|
||||
)
|
||||
|
||||
return profile
|
||||
|
||||
def _feature_rows(self, features):
|
||||
rows = []
|
||||
|
||||
for f in features:
|
||||
socket = f.socket.name
|
||||
sensor = str(f.sensorName or "").upper()
|
||||
|
||||
rows.append({
|
||||
"socket": socket,
|
||||
"sensor": sensor,
|
||||
"width": int(getattr(f, "width", 0) or 0),
|
||||
"height": int(getattr(f, "height", 0) or 0),
|
||||
"role": self.roles.get(socket, "unknown"),
|
||||
})
|
||||
|
||||
return rows
|
||||
|
||||
def _validate_connected_hardware(self, features):
|
||||
rows = self._feature_rows(features)
|
||||
by_socket = {row["socket"]: row for row in rows}
|
||||
|
||||
if not self.product_contract:
|
||||
return rows
|
||||
|
||||
if (
|
||||
self.expected_device_mx_id is not None
|
||||
and self.mx_id is not None
|
||||
and self.mx_id != self.expected_device_mx_id
|
||||
):
|
||||
raise RuntimeError(
|
||||
"OAK conectada não corresponde ao módulo calibrado: "
|
||||
f"connected={self.mx_id}, calibrated={self.expected_device_mx_id}"
|
||||
)
|
||||
|
||||
errors = []
|
||||
|
||||
for role, expected in self.expected_camera_hardware.items():
|
||||
row = by_socket.get(expected["socket"])
|
||||
|
||||
if row is None:
|
||||
errors.append(f"{role}: {expected['socket']} ausente")
|
||||
continue
|
||||
|
||||
if row["sensor"] != expected["sensor"]:
|
||||
errors.append(
|
||||
f"{role}: {expected['socket']} sensor={row['sensor']}, "
|
||||
f"esperado={expected['sensor']}"
|
||||
)
|
||||
|
||||
if row["width"] > 0 and row["height"] > 0:
|
||||
advertised = [row["width"], row["height"]]
|
||||
|
||||
if advertised != expected["size"]:
|
||||
errors.append(
|
||||
f"{role}: {expected['socket']}/{row['sensor']} anunciou "
|
||||
f"{advertised}, esperado={expected['size']}"
|
||||
)
|
||||
|
||||
if errors:
|
||||
raise RuntimeError(
|
||||
"Hardware OAK não corresponde ao module_params homologado:\n - "
|
||||
+ "\n - ".join(errors)
|
||||
)
|
||||
|
||||
return rows
|
||||
return json.load(f)
|
||||
|
||||
def _default_controls_for_role(self, role: str):
|
||||
role = str(role).lower()
|
||||
|
|
@ -579,31 +212,12 @@ class OakFcc3Manager:
|
|||
|
||||
with dai.Device(dev_info) as dev:
|
||||
result = []
|
||||
features = dev.getConnectedCameraFeatures()
|
||||
|
||||
for row in self._feature_rows(features):
|
||||
role = str(row.get("role", "unknown")).lower()
|
||||
expected = self.expected_camera_hardware.get(role)
|
||||
|
||||
for f in dev.getConnectedCameraFeatures():
|
||||
result.append({
|
||||
**row,
|
||||
"expected": copy.deepcopy(expected),
|
||||
"matches_product_contract": (
|
||||
None
|
||||
if not self.product_contract
|
||||
else bool(
|
||||
expected is not None
|
||||
and row["socket"] == expected["socket"]
|
||||
and row["sensor"] == expected["sensor"]
|
||||
and (
|
||||
row["width"] <= 0
|
||||
or row["height"] <= 0
|
||||
or [row["width"], row["height"]] == expected["size"]
|
||||
)
|
||||
)
|
||||
),
|
||||
"socket": f.socket.name,
|
||||
"sensor": f.sensorName,
|
||||
"role": self.roles.get(f.socket.name, "unknown"),
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
def _device_id_from_info(self, dev_info):
|
||||
|
|
@ -677,48 +291,35 @@ class OakFcc3Manager:
|
|||
|
||||
def _create_camera_node_classic(self, socket, sensor_name: str, role: str):
|
||||
"""
|
||||
RAW/PREVIEW sensor-aware.
|
||||
Fluxo clássico.
|
||||
|
||||
Produto:
|
||||
OV9782 -> ColorCamera THE_800_P 1280x800
|
||||
AR0234 -> ColorCamera THE_1200_P 1920x1200
|
||||
OV9282 -> MonoCamera THE_800_P 1280x800
|
||||
RGB/OV9782:
|
||||
ColorCamera raw para RAW_BRUTO.
|
||||
|
||||
No produto não existe fallback silencioso de resolução.
|
||||
MONO/OV9282:
|
||||
MonoCamera raw quando disponível.
|
||||
"""
|
||||
sensor = str(sensor_name or "").upper()
|
||||
role = str(role or "").lower()
|
||||
sensor_name_u = str(sensor_name or "").upper()
|
||||
role_u = str(role or "").lower()
|
||||
|
||||
if self.product_contract:
|
||||
profile = self._sensor_profile(sensor, role=role)
|
||||
else:
|
||||
profile = self.SENSOR_PROFILES.get(sensor)
|
||||
is_rgb = (
|
||||
role_u == "rgb"
|
||||
or "OV9782" in sensor_name_u
|
||||
or socket == dai.CameraBoardSocket.CAM_A
|
||||
)
|
||||
|
||||
if profile is None:
|
||||
profile = {
|
||||
"kind": "color" if role == "rgb" else "mono",
|
||||
"resolution_name": "THE_800_P",
|
||||
"width": self.LEGACY_SENSOR_W,
|
||||
"height": self.LEGACY_SENSOR_H,
|
||||
}
|
||||
|
||||
if profile["kind"] == "color":
|
||||
if is_rgb:
|
||||
cam = self.pipeline.createColorCamera()
|
||||
cam.setBoardSocket(socket)
|
||||
|
||||
resolution = getattr(
|
||||
dai.ColorCameraProperties.SensorResolution,
|
||||
profile["resolution_name"],
|
||||
None,
|
||||
)
|
||||
try:
|
||||
cam.setResolution(dai.ColorCameraProperties.SensorResolution.THE_800_P)
|
||||
except Exception:
|
||||
try:
|
||||
cam.setResolution(dai.ColorCameraProperties.SensorResolution.THE_1080_P)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if resolution is None:
|
||||
raise RuntimeError(
|
||||
f"DepthAI não expõe ColorCamera "
|
||||
f"{profile['resolution_name']} para {sensor}."
|
||||
)
|
||||
|
||||
cam.setResolution(resolution)
|
||||
cam.setInterleaved(False)
|
||||
cam.setColorOrder(dai.ColorCameraProperties.ColorOrder.RGB)
|
||||
cam.setFps(float(self.fps))
|
||||
|
|
@ -730,42 +331,31 @@ class OakFcc3Manager:
|
|||
except Exception:
|
||||
return cam, cam.preview
|
||||
|
||||
if not hasattr(cam, "raw"):
|
||||
raise RuntimeError(
|
||||
f"ColorCamera {sensor} não expõe raw output."
|
||||
)
|
||||
|
||||
return cam, cam.raw
|
||||
|
||||
mono = self.pipeline.create(dai.node.MonoCamera)
|
||||
mono.setBoardSocket(socket)
|
||||
|
||||
resolution = getattr(
|
||||
dai.MonoCameraProperties.SensorResolution,
|
||||
profile["resolution_name"],
|
||||
None,
|
||||
)
|
||||
try:
|
||||
mono.setResolution(dai.MonoCameraProperties.SensorResolution.THE_800_P)
|
||||
except Exception:
|
||||
try:
|
||||
mono.setResolution(dai.MonoCameraProperties.SensorResolution.THE_720_P)
|
||||
except Exception:
|
||||
try:
|
||||
mono.setResolution(dai.MonoCameraProperties.SensorResolution.THE_400_P)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if resolution is None:
|
||||
raise RuntimeError(
|
||||
f"DepthAI não expõe MonoCamera "
|
||||
f"{profile['resolution_name']} para {sensor}."
|
||||
)
|
||||
|
||||
mono.setResolution(resolution)
|
||||
mono.setFps(float(self.fps))
|
||||
|
||||
if self._is_preview_mode():
|
||||
return mono, mono.out
|
||||
|
||||
if not hasattr(mono, "raw"):
|
||||
if self.product_contract:
|
||||
raise RuntimeError(
|
||||
f"MonoCamera {sensor} não expõe raw output."
|
||||
)
|
||||
return mono, mono.out
|
||||
if hasattr(mono, "raw"):
|
||||
return mono, mono.raw
|
||||
|
||||
return mono, mono.raw
|
||||
return mono, mono.out
|
||||
|
||||
def _create_imu_node(self, pipeline):
|
||||
self.has_imu_pipeline = False
|
||||
|
|
@ -1306,13 +896,6 @@ class OakFcc3Manager:
|
|||
self.pipeline = dai.Pipeline()
|
||||
|
||||
features = self.device.getConnectedCameraFeatures()
|
||||
self._validate_connected_hardware(features)
|
||||
|
||||
if self.product_contract and self._is_multispec_mode():
|
||||
raise RuntimeError(
|
||||
"MULTISPEC alinhado na OAK é legado e não faz parte do "
|
||||
"contrato de produção. Use RAW_BRUTO."
|
||||
)
|
||||
|
||||
self.queues.clear()
|
||||
self.buffers.clear()
|
||||
|
|
@ -1360,44 +943,11 @@ class OakFcc3Manager:
|
|||
self.buffers[cam_id] = deque(maxlen=self.buffer_size)
|
||||
self.control_queues[cam_id] = None
|
||||
|
||||
sensor_u = str(f.sensorName or "").upper()
|
||||
if self.product_contract or sensor_u in self.SENSOR_PROFILES:
|
||||
profile = self._sensor_profile(sensor_u, role=role)
|
||||
else:
|
||||
profile = {
|
||||
"kind": "unknown",
|
||||
"width": int(getattr(f, "width", 0) or self.LEGACY_SENSOR_W),
|
||||
"height": int(getattr(f, "height", 0) or self.LEGACY_SENSOR_H),
|
||||
"resolution_name": None,
|
||||
}
|
||||
|
||||
self.camera_info[cam_id] = {
|
||||
"id": cam_id,
|
||||
"socket": socket_name,
|
||||
"sensor": sensor_u,
|
||||
"sensor": f.sensorName,
|
||||
"role": role,
|
||||
"native_width": int(profile["width"]),
|
||||
"native_height": int(profile["height"]),
|
||||
"native_size": [
|
||||
int(profile["width"]),
|
||||
int(profile["height"]),
|
||||
],
|
||||
"sensor_kind": profile["kind"],
|
||||
"resolution_mode": profile.get("resolution_name"),
|
||||
"bayer_pattern": (
|
||||
self.bayer_pattern
|
||||
if str(role).lower() == "rgb"
|
||||
else None
|
||||
),
|
||||
"product_expected": (
|
||||
copy.deepcopy(
|
||||
self.expected_camera_hardware.get(
|
||||
str(role).lower()
|
||||
)
|
||||
)
|
||||
if self.product_contract
|
||||
else None
|
||||
),
|
||||
}
|
||||
|
||||
self._validate_capture_mode()
|
||||
|
|
@ -1599,14 +1149,6 @@ class OakFcc3Manager:
|
|||
"height": self.height,
|
||||
"sensor_width": self.sensor_width,
|
||||
"sensor_height": self.sensor_height,
|
||||
"sensor_size_by_role": copy.deepcopy(self.sensor_size_by_role),
|
||||
"camera_hardware_expected": copy.deepcopy(self.expected_camera_hardware),
|
||||
"bayer_pattern": self.bayer_pattern,
|
||||
"product_contract": bool(self.product_contract),
|
||||
"require_product_contract": bool(self.require_product_contract),
|
||||
"manager_version": OAK_FCC3_MANAGER_VERSION,
|
||||
"module_params_schema": (self.module_params or {}).get("schema"),
|
||||
"module_calibration_json": self.module_calibration_json,
|
||||
"frame_type": self.frame_type,
|
||||
"output_dtype": self.output_dtype,
|
||||
"capture_mode": self.capture_mode,
|
||||
|
|
@ -2184,32 +1726,6 @@ class OakFcc3Manager:
|
|||
item["shape"] = list(arr.shape)
|
||||
item["dtype"] = str(arr.dtype)
|
||||
item["packed"] = True
|
||||
item["native_width"] = int(
|
||||
item.get("native_width", item["width"])
|
||||
)
|
||||
item["native_height"] = int(
|
||||
item.get("native_height", item["height"])
|
||||
)
|
||||
item["native_size"] = [
|
||||
item["native_width"],
|
||||
item["native_height"],
|
||||
]
|
||||
|
||||
if str(item.get("role", "")).lower() == "rgb":
|
||||
item["bayer_pattern"] = self.bayer_pattern
|
||||
|
||||
if self.product_contract:
|
||||
role = str(item.get("role", "")).lower()
|
||||
expected = self.expected_camera_hardware.get(role)
|
||||
|
||||
if expected is not None:
|
||||
actual = [int(item["width"]), int(item["height"])]
|
||||
|
||||
if actual != expected["size"]:
|
||||
raise RuntimeError(
|
||||
f"{cam_id}/{role}: RAW frame {actual} != "
|
||||
f"hardware homologado {expected['size']}"
|
||||
)
|
||||
|
||||
camera_info[cam_id] = item
|
||||
|
||||
|
|
@ -2217,18 +1733,6 @@ class OakFcc3Manager:
|
|||
"frame_id": self.frame_id,
|
||||
"backend": "oak_fcc3",
|
||||
"frame_type": self.frame_type,
|
||||
"product_contract": bool(self.product_contract),
|
||||
"module_params_schema": (self.module_params or {}).get("schema"),
|
||||
"module_calibration_json": self.module_calibration_json,
|
||||
"device_mx_id": self.mx_id,
|
||||
"reference_camera": "rgb",
|
||||
"sensor_size_by_role": copy.deepcopy(self.sensor_size_by_role),
|
||||
"camera_hardware": copy.deepcopy(
|
||||
self.expected_camera_hardware
|
||||
if self.product_contract
|
||||
else {}
|
||||
),
|
||||
"bayer_pattern": self.bayer_pattern,
|
||||
"capture_mode": self.capture_mode,
|
||||
"output_dtype": self.output_dtype,
|
||||
"dtype": self.output_dtype,
|
||||
|
|
@ -2417,65 +1921,18 @@ class OakFcc3Manager:
|
|||
return result
|
||||
|
||||
def apply_initial_camera_controls_to_node(self, cam, cam_id):
|
||||
"""
|
||||
Aplica Startup Profile antes de startPipeline().
|
||||
|
||||
O Client pode reaplicar depois do start como confirmação, mas os
|
||||
primeiros frames já nascem sob a política homologada.
|
||||
"""
|
||||
ctrl_state = self.camera_controls.get(cam_id, {})
|
||||
|
||||
ae = bool(ctrl_state.get("ae_enable", False))
|
||||
awb = bool(ctrl_state.get("awb_enable", False))
|
||||
exp_us = int(ctrl_state.get("exposure_time_us") or 15000)
|
||||
gain = float(ctrl_state.get("analogue_gain") or 1.0)
|
||||
|
||||
if ae:
|
||||
fn = getattr(cam.initialControl, "setAutoExposureEnable", None)
|
||||
|
||||
if callable(fn):
|
||||
fn()
|
||||
elif self.product_contract:
|
||||
raise RuntimeError(
|
||||
f"{cam_id}: DepthAI sem setAutoExposureEnable no initialControl."
|
||||
)
|
||||
else:
|
||||
if not ae:
|
||||
cam.initialControl.setManualExposure(
|
||||
exp_us,
|
||||
self._gain_to_iso(gain),
|
||||
)
|
||||
|
||||
role = str(self.roles.get(cam_id, "")).lower()
|
||||
|
||||
if role == "rgb":
|
||||
if awb:
|
||||
fn = getattr(
|
||||
cam.initialControl,
|
||||
"setAutoWhiteBalanceLock",
|
||||
None,
|
||||
)
|
||||
|
||||
if callable(fn):
|
||||
fn(False)
|
||||
else:
|
||||
try:
|
||||
cam.initialControl.setAutoWhiteBalanceMode(
|
||||
dai.CameraControl.AutoWhiteBalanceMode.AUTO
|
||||
)
|
||||
except Exception:
|
||||
if self.product_contract:
|
||||
raise RuntimeError(
|
||||
f"{cam_id}: não foi possível habilitar AWB inicial."
|
||||
)
|
||||
else:
|
||||
fn = getattr(
|
||||
cam.initialControl,
|
||||
"setAutoWhiteBalanceLock",
|
||||
None,
|
||||
)
|
||||
if callable(fn):
|
||||
fn(True)
|
||||
|
||||
def _send_control(self, cam_id, ctrl):
|
||||
if cam_id not in self.control_queues:
|
||||
raise RuntimeError(f"Fila de controle não existe para {cam_id}")
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,15 +1,17 @@
|
|||
using OperationControl.Models;
|
||||
using OperationControl.Mapas;
|
||||
using OperationControl.Mapas.Services;
|
||||
using OperationControl.Models;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media.Animation;
|
||||
using UserControl = System.Windows.Controls.UserControl;
|
||||
using KeyEventArgs = System.Windows.Input.KeyEventArgs;
|
||||
using Binding = System.Windows.Data.Binding;
|
||||
using Application = System.Windows.Application;
|
||||
using Binding = System.Windows.Data.Binding;
|
||||
using KeyEventArgs = System.Windows.Input.KeyEventArgs;
|
||||
using Point = System.Windows.Point;
|
||||
using Size = System.Windows.Size;
|
||||
using UserControl = System.Windows.Controls.UserControl;
|
||||
|
||||
namespace OperationControl.Controls
|
||||
{
|
||||
|
|
@ -315,7 +317,8 @@ namespace OperationControl.Controls
|
|||
}
|
||||
else
|
||||
{
|
||||
if (!(TgManual.IsChecked ?? false) && !(((App)Application.Current).Shell.Main?.MAP?.RuasMapaCarregado?.Any(x => x.Selected) ?? false))
|
||||
var ruasPercorrer = ((App)Application.Current).Shell.Dock?._vm?._mapas?.ObterRuasPercorrer() ?? new List<string>();
|
||||
if (!(TgManual.IsChecked ?? false) && !ruasPercorrer.Any())
|
||||
{
|
||||
System.Windows.MessageBox.Show("Para iniciar a operação automática, é necessário carregar um mapa e selecionar as ruas onde o equipamento irá operar!", "Parametrização", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -2,9 +2,18 @@
|
|||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
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"
|
||||
x:Name="Root"
|
||||
MinHeight="100"
|
||||
MinWidth="300">
|
||||
MinWidth="300"
|
||||
Focusable="True"
|
||||
FocusVisualStyle="{x:Null}"
|
||||
PreviewMouseLeftButtonDown="Root_PreviewMouseLeftButtonDown"
|
||||
PreviewMouseMove="Root_PreviewMouseMove"
|
||||
PreviewMouseLeftButtonUp="Root_PreviewMouseLeftButtonUp"
|
||||
LostMouseCapture="Root_LostMouseCapture"
|
||||
PreviewKeyDown="Root_PreviewKeyDown"
|
||||
>
|
||||
<Grid>
|
||||
<!-- Mapsui MapControl preenche todo o espaço disponível (responsivo) -->
|
||||
<m:MapControl x:Name="Mapa"/>
|
||||
|
|
@ -17,7 +26,70 @@
|
|||
<ComboBox x:Name="CmbBaseMap" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,10,70,0" Width="189" SelectionChanged="OnBaseMapChanged"/>
|
||||
<CheckBox x:Name="chbAcompanhar" Content="Seguir" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,13,10,0"/>
|
||||
|
||||
<Button Content="Carregar Mapa" HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="0,0,10,10" Padding="8,4" Background="#2ECC71" Foreground="White" FontWeight="Bold" Click="OnLoadMapClicked" Visibility="Collapsed"/>
|
||||
<Button Content="Carregar Mapa" HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="0,0,10,10" Padding="8,4" Background="#2ECC71" Foreground="White" FontWeight="Bold" Visibility="Collapsed"/>
|
||||
<!--<Button Content="Centralizar" HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="0,0,10,10" Padding="8,4" Background="#2ECC71" Foreground="White" FontWeight="Bold" Click="OnCenterAreaClicked"/>-->
|
||||
|
||||
<!-- INTERAÇÃO VISUAL DO EDITOR -->
|
||||
<Canvas x:Name="EditorInteractionCanvas" Background="Transparent" IsHitTestVisible="False" Panel.ZIndex="90">
|
||||
|
||||
|
||||
<!--
|
||||
Área interna do recorte.
|
||||
|
||||
O Polygon já mostra visualmente
|
||||
como o último ponto será fechado
|
||||
até o primeiro.
|
||||
-->
|
||||
<Polygon
|
||||
x:Name="EditorCutPolygonFill"
|
||||
Visibility="Collapsed"
|
||||
Fill="#38FF3B30"
|
||||
Stroke="#99FF6B60"
|
||||
StrokeThickness="1"
|
||||
IsHitTestVisible="False"/>
|
||||
|
||||
|
||||
<!-- Linha realmente percorrida pelo mouse -->
|
||||
<Polyline
|
||||
x:Name="EditorCutPolyline"
|
||||
Visibility="Collapsed"
|
||||
Stroke="#FFFF675C"
|
||||
StrokeThickness="3"
|
||||
StrokeLineJoin="Round"
|
||||
StrokeStartLineCap="Round"
|
||||
StrokeEndLineCap="Round"
|
||||
IsHitTestVisible="False"/>
|
||||
|
||||
|
||||
<!--
|
||||
Ligação visual do último ponto
|
||||
até o primeiro.
|
||||
-->
|
||||
<Line
|
||||
x:Name="EditorCutClosingLine"
|
||||
Visibility="Collapsed"
|
||||
Stroke="#CCFFB0AA"
|
||||
StrokeThickness="2"
|
||||
StrokeDashArray="4,3"
|
||||
IsHitTestVisible="False"/>
|
||||
|
||||
|
||||
<Canvas x:Name="EditorCutVerticesCanvas" IsHitTestVisible="False"/>
|
||||
|
||||
|
||||
<!-- Seleção retangular existente -->
|
||||
<Border
|
||||
x:Name="EditorSelectionRectangle"
|
||||
Visibility="Collapsed"
|
||||
Background="#334E8EF7"
|
||||
BorderBrush="#FF6BA6FF"
|
||||
BorderThickness="2"
|
||||
CornerRadius="2"/>
|
||||
|
||||
</Canvas>
|
||||
|
||||
<!-- COCKPIT DO EDITOR DE MAPAS -->
|
||||
<editing:MapaEditorOverlay x:Name="EditorOverlay" Panel.ZIndex="100"/>
|
||||
|
||||
</Grid>
|
||||
</UserControl>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,722 @@
|
|||
using NetTopologySuite.Geometries;
|
||||
using OperationControl.Controls;
|
||||
using OperationControl.Mapas.Editing;
|
||||
using OperationControl.Mapas.Interaction;
|
||||
|
||||
namespace OperationControl.Mapas
|
||||
{
|
||||
public sealed partial class MapaOperacaoController
|
||||
{
|
||||
/*
|
||||
* =========================================================
|
||||
* CONFIGURAÇÃO DE INTERAÇÃO
|
||||
* =========================================================
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// Define explicitamente como o MapViewControl deve
|
||||
/// interpretar a interação do operador.
|
||||
///
|
||||
/// Se a View estiver em uma sessão de edição ativa,
|
||||
/// o modo Edicao tem prioridade até a sessão terminar.
|
||||
/// </summary>
|
||||
public void DefinirModoInteracao(
|
||||
MapViewControl? mapaControl,
|
||||
MapaModoInteracao modo)
|
||||
{
|
||||
if (
|
||||
mapaControl?.Mapa?.Map == null
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
RegistrarMapa(
|
||||
mapaControl
|
||||
);
|
||||
|
||||
|
||||
MapaModoInteracao modoEfetivo =
|
||||
_estadoEdicao.EmEdicao &&
|
||||
_mapasEmEdicao.Contains(
|
||||
mapaControl
|
||||
)
|
||||
? MapaModoInteracao.Edicao
|
||||
: modo;
|
||||
|
||||
|
||||
AplicarModoInteracao(
|
||||
mapaControl,
|
||||
modoEfetivo
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private void AplicarModoInteracao(
|
||||
MapViewControl mapaControl,
|
||||
MapaModoInteracao modo)
|
||||
{
|
||||
if (
|
||||
mapaControl.ModoInteracao ==
|
||||
modo
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var anterior =
|
||||
mapaControl.ModoInteracao;
|
||||
|
||||
|
||||
mapaControl.DefinirModoInteracao(
|
||||
modo
|
||||
);
|
||||
|
||||
|
||||
Log(
|
||||
"[MAPA][INTERACAO] " +
|
||||
$"Anterior={anterior} | " +
|
||||
$"Atual={modo}"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* =========================================================
|
||||
* RUA
|
||||
* =========================================================
|
||||
*/
|
||||
|
||||
private void OnMapaNovoRuaClicada(string ruaId)
|
||||
{
|
||||
try
|
||||
{
|
||||
/*
|
||||
* =============================================
|
||||
* MODO EDIÇÃO
|
||||
* =============================================
|
||||
*
|
||||
* A seleção operacional fica completamente
|
||||
* intocada.
|
||||
*/
|
||||
if (_estadoEdicao.EmEdicao)
|
||||
{
|
||||
/*
|
||||
* Uma rua só responde ao clique direto
|
||||
* enquanto a ferramenta Selecionar
|
||||
* estiver ativa.
|
||||
*/
|
||||
if (
|
||||
_ferramentaEdicaoAtual !=
|
||||
MapaFerramentaEdicao.Selecionar
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AlternarRuaEdicao(
|
||||
ruaId
|
||||
);
|
||||
|
||||
|
||||
Log(
|
||||
"[MAPA][EDICAO][CLIQUE] " +
|
||||
$"Rua={ruaId} | " +
|
||||
$"Selecionadas=" +
|
||||
$"{QuantidadeRuasSelecionadasEdicao}"
|
||||
);
|
||||
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* =============================================
|
||||
* MODO NORMAL
|
||||
* =============================================
|
||||
*/
|
||||
AlternarRuaOperacao(
|
||||
ruaId
|
||||
);
|
||||
|
||||
|
||||
string ruas =
|
||||
string.Join(
|
||||
" -> ",
|
||||
ObterRuasPercorrer()
|
||||
);
|
||||
|
||||
|
||||
Log(
|
||||
"[MAPA][CLIQUE] " +
|
||||
$"Rua={ruaId} | " +
|
||||
$"Percurso=[{ruas}]"
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log(
|
||||
"[MAPA][CLIQUE][ERRO] " +
|
||||
ex.Message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* =========================================================
|
||||
* RETORNO
|
||||
* =========================================================
|
||||
*/
|
||||
|
||||
private void OnMapaNovoPontoRetornoClicado(
|
||||
double latitude,
|
||||
double longitude)
|
||||
{
|
||||
try
|
||||
{
|
||||
AlternarPontoRetorno(
|
||||
latitude,
|
||||
longitude
|
||||
);
|
||||
|
||||
|
||||
double distancia =
|
||||
ObterDistanciaRotaRetornoMetros();
|
||||
|
||||
|
||||
Log(
|
||||
"[MAPA][RETORNO][CLIQUE] " +
|
||||
$"Pontos={QuantidadePontosRetorno} | " +
|
||||
$"Distancia={distancia:F2} m"
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log(
|
||||
"[MAPA][RETORNO][CLIQUE][ERRO] " +
|
||||
ex.Message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* =========================================================
|
||||
* ENTIDADE
|
||||
* =========================================================
|
||||
*/
|
||||
|
||||
private void OnMapaNovaEntidadeClicada(
|
||||
string entidadeId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var entidade =
|
||||
ObterEntidade(
|
||||
entidadeId
|
||||
);
|
||||
|
||||
|
||||
if (entidade == null)
|
||||
return;
|
||||
|
||||
|
||||
Log(
|
||||
"[MAPA][ENTIDADE][CLIQUE] " +
|
||||
$"Id={entidade.Id} | " +
|
||||
$"Tipo={entidade.Tipo}"
|
||||
);
|
||||
|
||||
|
||||
/*
|
||||
* Fim da responsabilidade do mapa.
|
||||
*
|
||||
* Dock decidirá o que significa
|
||||
* selecionar Base ou Rover.
|
||||
*/
|
||||
EntidadeClicada?
|
||||
.Invoke(
|
||||
entidade.Id
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log(
|
||||
"[MAPA][ENTIDADE][CLIQUE][ERRO] " +
|
||||
ex.Message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* =========================================================
|
||||
* VIEWPORT
|
||||
* =========================================================
|
||||
*/
|
||||
|
||||
private void OnMapaNovoViewportAlterado(
|
||||
MapViewControl mapaControl)
|
||||
{
|
||||
/*
|
||||
* Somente entidades dependem da resolução
|
||||
* atual para manter tamanho visual adequado.
|
||||
*/
|
||||
RenderizarEntidadesDinamicasNoMapa(
|
||||
mapaControl
|
||||
);
|
||||
|
||||
|
||||
mapaControl
|
||||
.Mapa
|
||||
.Refresh();
|
||||
}
|
||||
|
||||
|
||||
private void OnMapaEditorFerramentaSelecionada(MapViewControl mapaControl, MapaFerramentaEdicao ferramenta)
|
||||
{
|
||||
if (
|
||||
!_estadoEdicao.EmEdicao ||
|
||||
!_mapasEmEdicao.Contains(
|
||||
mapaControl
|
||||
)
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
DefinirFerramentaEdicao(
|
||||
ferramenta
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private void OnMapaEditorLimparSelecaoSolicitado(MapViewControl mapaControl)
|
||||
{
|
||||
if (
|
||||
!_estadoEdicao.EmEdicao ||
|
||||
!_mapasEmEdicao.Contains(
|
||||
mapaControl
|
||||
)
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
LimparSelecaoEdicao();
|
||||
}
|
||||
|
||||
|
||||
private void OnMapaEditorRetanguloSelecionado(MapViewControl mapaControl, MapaRetanguloSelecao retangulo)
|
||||
{
|
||||
if (
|
||||
!_estadoEdicao.EmEdicao ||
|
||||
!_mapasEmEdicao.Contains(
|
||||
mapaControl
|
||||
) ||
|
||||
_ferramentaEdicaoAtual !=
|
||||
MapaFerramentaEdicao
|
||||
.SelecionarRetangulo
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
var encontrados =
|
||||
_edicaoService
|
||||
.SelecionarPorRetangulo(
|
||||
_estadoEdicao,
|
||||
|
||||
retangulo
|
||||
.MinLatitude,
|
||||
|
||||
retangulo
|
||||
.MinLongitude,
|
||||
|
||||
retangulo
|
||||
.MaxLatitude,
|
||||
|
||||
retangulo
|
||||
.MaxLongitude,
|
||||
|
||||
substituirSelecao:
|
||||
true
|
||||
);
|
||||
|
||||
|
||||
foreach (
|
||||
var mapa
|
||||
in _mapasEmEdicao
|
||||
.ToArray())
|
||||
{
|
||||
if (
|
||||
_rendersMapa.TryGetValue(
|
||||
mapa,
|
||||
out var render
|
||||
)
|
||||
)
|
||||
{
|
||||
_edicaoRenderService
|
||||
.AplicarSelecao(
|
||||
render,
|
||||
_estadoEdicao
|
||||
);
|
||||
|
||||
|
||||
mapa.Mapa.Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
SincronizarCockpitEdicao();
|
||||
|
||||
|
||||
EstadoEdicaoAlterado?
|
||||
.Invoke();
|
||||
|
||||
|
||||
Log(
|
||||
"[MAPA][EDICAO][RETANGULO] " +
|
||||
$"Encontradas={encontrados.Count} | " +
|
||||
$"Selecionadas=" +
|
||||
$"{QuantidadeRuasSelecionadasEdicao}"
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log(
|
||||
"[MAPA][EDICAO][RETANGULO][ERRO] " +
|
||||
ex.Message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void OnMapaEditorPoligonoRecorteDefinido(MapViewControl mapaControl, MapaPoligonoRecorte recorte)
|
||||
{
|
||||
bool ferramentaPoligono =
|
||||
_ferramentaEdicaoAtual ==
|
||||
MapaFerramentaEdicao.RecorteLivre
|
||||
||
|
||||
_ferramentaEdicaoAtual ==
|
||||
MapaFerramentaEdicao.RecortePorPontos;
|
||||
|
||||
|
||||
if (
|
||||
!_estadoEdicao.EmEdicao ||
|
||||
!_mapasEmEdicao.Contains(
|
||||
mapaControl
|
||||
) ||
|
||||
!ferramentaPoligono
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (
|
||||
recorte == null ||
|
||||
recorte.QuantidadePontos < 3
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
/*
|
||||
* Fronteira UI → domínio.
|
||||
*
|
||||
* DTO:
|
||||
* Latitude / Longitude
|
||||
*
|
||||
* NTS:
|
||||
* X = longitude
|
||||
* Y = latitude
|
||||
*/
|
||||
Coordinate[] pontos =
|
||||
recorte
|
||||
.Pontos
|
||||
.Select(
|
||||
p =>
|
||||
new Coordinate(
|
||||
p.Longitude,
|
||||
p.Latitude
|
||||
)
|
||||
)
|
||||
.ToArray();
|
||||
|
||||
|
||||
/*
|
||||
* V1:
|
||||
* o próprio polígono define o escopo.
|
||||
*
|
||||
* Depois adicionaremos a opção
|
||||
* "Somente ruas selecionadas".
|
||||
*/
|
||||
if (_estadoRecorte.SomenteSelecionadas && !_estadoEdicao.PossuiSelecao)
|
||||
{
|
||||
Log(
|
||||
"[MAPA][EDICAO][RECORTE][IGNORADO] " +
|
||||
"Escopo=SomenteSelecionadas | " +
|
||||
"Nenhuma rua selecionada."
|
||||
);
|
||||
|
||||
|
||||
SincronizarCockpitEdicao();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
MapaResultadoRecorte resultado =
|
||||
_edicaoService
|
||||
.RecortarPorPoligono(
|
||||
_estadoEdicao,
|
||||
pontos,
|
||||
somenteSelecionadas: _estadoRecorte.SomenteSelecionadas
|
||||
);
|
||||
|
||||
|
||||
/*
|
||||
* Mesmo sem alteração podemos ter
|
||||
* ruas ambíguas importantes para log.
|
||||
*/
|
||||
if (!resultado.HouveAlteracao)
|
||||
{
|
||||
string tipoRecorte =
|
||||
_ferramentaEdicaoAtual ==
|
||||
MapaFerramentaEdicao.RecorteLivre
|
||||
? "RECORTE-LIVRE"
|
||||
: "RECORTE-PONTOS";
|
||||
|
||||
Log(
|
||||
$"[MAPA][EDICAO][{tipoRecorte}] " +
|
||||
$"Escopo={_estadoRecorte.Escopo} | " +
|
||||
resultado
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* É alteração estrutural/geometria.
|
||||
*
|
||||
* Não basta atualizar estilo.
|
||||
*/
|
||||
RenderizarDocumentoEdicaoAtual();
|
||||
|
||||
|
||||
SincronizarCockpitEdicao();
|
||||
|
||||
|
||||
EstadoEdicaoAlterado?
|
||||
.Invoke();
|
||||
|
||||
|
||||
Log(
|
||||
"[MAPA][EDICAO][RECORTE-LIVRE] " +
|
||||
resultado
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
/*
|
||||
* O MapaEdicaoService já restaura
|
||||
* o snapshot transacional em caso
|
||||
* de erro.
|
||||
*/
|
||||
Log(
|
||||
"[MAPA][EDICAO][RECORTE-LIVRE][ERRO] " +
|
||||
ex.Message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void OnMapaEditorDesfazerSolicitado(MapViewControl mapaControl)
|
||||
{
|
||||
if (
|
||||
!_estadoEdicao.EmEdicao ||
|
||||
!_mapasEmEdicao.Contains(
|
||||
mapaControl
|
||||
)
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
DesfazerEdicao();
|
||||
}
|
||||
|
||||
|
||||
private void OnMapaEditorRefazerSolicitado(MapViewControl mapaControl)
|
||||
{
|
||||
if (
|
||||
!_estadoEdicao.EmEdicao ||
|
||||
!_mapasEmEdicao.Contains(
|
||||
mapaControl
|
||||
)
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
RefazerEdicao();
|
||||
}
|
||||
|
||||
|
||||
private void OnMapaEditorExcluirSelecionadasSolicitado(MapViewControl mapaControl)
|
||||
{
|
||||
if (
|
||||
!_estadoEdicao.EmEdicao ||
|
||||
!_mapasEmEdicao.Contains(
|
||||
mapaControl
|
||||
) ||
|
||||
!_estadoEdicao.PossuiSelecao
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
RemoverRuasSelecionadasEdicao();
|
||||
}
|
||||
|
||||
|
||||
private void OnMapaEditorAplicarEdicaoSolicitado(MapViewControl mapaControl)
|
||||
{
|
||||
if (!_estadoEdicao.EmEdicao || !_mapasEmEdicao.Contains(mapaControl))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AplicarEdicao();
|
||||
}
|
||||
|
||||
|
||||
private void OnMapaEditorCancelarEdicaoSolicitado(MapViewControl mapaControl)
|
||||
{
|
||||
if (!_estadoEdicao.EmEdicao || !_mapasEmEdicao.Contains(mapaControl))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CancelarEdicao(mapaControl);
|
||||
}
|
||||
|
||||
|
||||
private void OnMapaEditorEscopoRecorteAlterado(MapViewControl mapaControl, MapaEscopoRecorte escopo)
|
||||
{
|
||||
if (!_estadoEdicao.EmEdicao || !_mapasEmEdicao.Contains(mapaControl))
|
||||
return;
|
||||
|
||||
_estadoRecorte.Escopo = escopo;
|
||||
|
||||
SincronizarCockpitEdicao();
|
||||
|
||||
|
||||
Log(
|
||||
"[MAPA][EDICAO][RECORTE][ESCOPO] " +
|
||||
$"Atual={escopo}"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private void OnMapaEditorBordaGnssAcaoSolicitada(MapViewControl mapaControl, MapaAcaoBordaGnss acao)
|
||||
{
|
||||
if (
|
||||
!_estadoEdicao.EmEdicao ||
|
||||
!_mapasEmEdicao.Contains(mapaControl) ||
|
||||
_ferramentaEdicaoAtual != MapaFerramentaEdicao.RecorteBordaGnss
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
switch (acao)
|
||||
{
|
||||
case MapaAcaoBordaGnss
|
||||
.RegistrarPonto:
|
||||
|
||||
RegistrarPontoBordaGnssManual();
|
||||
break;
|
||||
|
||||
|
||||
case MapaAcaoBordaGnss
|
||||
.RemoverUltimo:
|
||||
|
||||
RemoverUltimoPontoBordaGnss();
|
||||
break;
|
||||
|
||||
|
||||
case MapaAcaoBordaGnss
|
||||
.Finalizar:
|
||||
|
||||
FinalizarCapturaBordaGnss();
|
||||
break;
|
||||
|
||||
|
||||
case MapaAcaoBordaGnss
|
||||
.Continuar:
|
||||
|
||||
ContinuarCapturaBordaGnss();
|
||||
break;
|
||||
|
||||
|
||||
case MapaAcaoBordaGnss
|
||||
.Descartar:
|
||||
|
||||
DescartarCapturaBordaGnss();
|
||||
break;
|
||||
|
||||
|
||||
case MapaAcaoBordaGnss.Aplicar:
|
||||
AplicarBordaGnss();
|
||||
break;
|
||||
|
||||
|
||||
case MapaAcaoBordaGnss.DefinirModoManual:
|
||||
DefinirModoCapturaBordaGnss(MapaModoCapturaGnss.Manual);
|
||||
break;
|
||||
|
||||
|
||||
case MapaAcaoBordaGnss.DefinirModoAutomatico:
|
||||
DefinirModoCapturaBordaGnss(MapaModoCapturaGnss.Automatico);
|
||||
break;
|
||||
|
||||
|
||||
case MapaAcaoBordaGnss.IniciarGravacaoAutomatica:
|
||||
IniciarGravacaoBordaGnssAutomatica();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void OnMapaEditorRestaurarFonteSolicitado(MapViewControl mapaControl)
|
||||
{
|
||||
if (!_estadoEdicao.EmEdicao || !_mapasEmEdicao.Contains(mapaControl))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RestaurarMapaFonteEdicao();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,599 @@
|
|||
using AgroBase.Models;
|
||||
using AgroBase.Models.Operacoes;
|
||||
using OperationControl.Controls;
|
||||
using OperationControl.Mapas.Models;
|
||||
using OperationControl.Models;
|
||||
using static AgroBase.Models.Enums;
|
||||
|
||||
namespace OperationControl.Mapas
|
||||
{
|
||||
public sealed partial class MapaOperacaoController
|
||||
{
|
||||
/*
|
||||
* =========================================================
|
||||
* BASE
|
||||
* =========================================================
|
||||
*/
|
||||
|
||||
public void AtualizarBase(
|
||||
GPSModel? dados,
|
||||
MapViewControl? mapaAtual = null)
|
||||
{
|
||||
if (dados == null)
|
||||
return;
|
||||
|
||||
|
||||
AtualizarSnapshotGnssLocal(dados);
|
||||
|
||||
|
||||
_entidadeService
|
||||
.DefinirBase(
|
||||
_estadoDinamico,
|
||||
VariaveisControleOperacao
|
||||
.BaseMarkerID,
|
||||
"Base",
|
||||
dados.Latitude,
|
||||
dados.Longitude,
|
||||
dados.OrientacaoReal,
|
||||
dados.LatitudeAnt,
|
||||
dados.LongitudeAnt
|
||||
);
|
||||
|
||||
|
||||
if (
|
||||
mapaAtual?.Mapa?.Map != null
|
||||
)
|
||||
{
|
||||
RegistrarMapa(
|
||||
mapaAtual
|
||||
);
|
||||
|
||||
|
||||
RenderizarReferenciasGnssNoMapa(
|
||||
mapaAtual
|
||||
);
|
||||
|
||||
|
||||
RenderizarEntidadesDinamicasNoMapa(
|
||||
mapaAtual
|
||||
);
|
||||
|
||||
|
||||
mapaAtual.Mapa.Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* =========================================================
|
||||
* ROVER
|
||||
* =========================================================
|
||||
*/
|
||||
|
||||
public void AtualizarRover(
|
||||
OperacaoParametrosModel? rover,
|
||||
MapViewControl? mapaAtual = null)
|
||||
{
|
||||
if (
|
||||
rover == null ||
|
||||
string.IsNullOrWhiteSpace(
|
||||
rover.RoverId
|
||||
)
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
AtualizarRoverNoEstadoDinamico(
|
||||
rover
|
||||
);
|
||||
|
||||
|
||||
AtualizarMovimentoRover(
|
||||
rover
|
||||
);
|
||||
|
||||
|
||||
if (
|
||||
mapaAtual?.Mapa?.Map != null
|
||||
)
|
||||
{
|
||||
RegistrarMapa(
|
||||
mapaAtual
|
||||
);
|
||||
|
||||
|
||||
/*
|
||||
* Ordem proposital.
|
||||
*/
|
||||
RenderizarReferenciasGnssNoMapa(
|
||||
mapaAtual
|
||||
);
|
||||
|
||||
|
||||
RenderizarMovimentoNoMapa(
|
||||
mapaAtual
|
||||
);
|
||||
|
||||
|
||||
RenderizarEntidadesDinamicasNoMapa(
|
||||
mapaAtual
|
||||
);
|
||||
|
||||
|
||||
mapaAtual.Mapa.Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void SincronizarRovers(
|
||||
IEnumerable<OperacaoParametrosModel>?
|
||||
rovers,
|
||||
MapViewControl? mapaAtual = null)
|
||||
{
|
||||
var lista =
|
||||
rovers?
|
||||
.Where(
|
||||
x =>
|
||||
x != null &&
|
||||
!string.IsNullOrWhiteSpace(
|
||||
x.RoverId
|
||||
)
|
||||
)
|
||||
.ToList()
|
||||
??
|
||||
new List<
|
||||
OperacaoParametrosModel>();
|
||||
|
||||
|
||||
var idsAtuais =
|
||||
lista
|
||||
.Select(
|
||||
x => x.RoverId.Trim()
|
||||
)
|
||||
.ToHashSet(
|
||||
StringComparer.OrdinalIgnoreCase
|
||||
);
|
||||
|
||||
|
||||
foreach (
|
||||
var rover
|
||||
in lista)
|
||||
{
|
||||
AtualizarRoverNoEstadoDinamico(
|
||||
rover
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Remove somente entidades dinâmicas.
|
||||
*
|
||||
* O rastro histórico continua disponível
|
||||
* mesmo que um rover desapareça momentaneamente
|
||||
* da lista da rede.
|
||||
*/
|
||||
var remover =
|
||||
_estadoDinamico
|
||||
.Entidades
|
||||
.Where(
|
||||
x =>
|
||||
x.Tipo ==
|
||||
MapaTipoEntidadeDinamica.Rover &&
|
||||
!idsAtuais.Contains(
|
||||
x.Id
|
||||
)
|
||||
)
|
||||
.Select(
|
||||
x => x.Id
|
||||
)
|
||||
.ToList();
|
||||
|
||||
|
||||
foreach (
|
||||
string id
|
||||
in remover)
|
||||
{
|
||||
_entidadeService
|
||||
.Remover(
|
||||
_estadoDinamico,
|
||||
id
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (
|
||||
mapaAtual?.Mapa?.Map != null
|
||||
)
|
||||
{
|
||||
RegistrarMapa(
|
||||
mapaAtual
|
||||
);
|
||||
|
||||
|
||||
RenderizarReferenciasGnssNoMapa(
|
||||
mapaAtual
|
||||
);
|
||||
|
||||
|
||||
RenderizarEntidadesDinamicasNoMapa(
|
||||
mapaAtual
|
||||
);
|
||||
|
||||
|
||||
mapaAtual.Mapa.Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void AtualizarRoverNoEstadoDinamico(
|
||||
OperacaoParametrosModel rover)
|
||||
{
|
||||
var obj =
|
||||
rover.DadosLeitura;
|
||||
|
||||
|
||||
var gnss =
|
||||
obj?.Gnss;
|
||||
|
||||
|
||||
_entidadeService
|
||||
.DefinirRover(
|
||||
_estadoDinamico,
|
||||
rover.RoverId,
|
||||
string.IsNullOrWhiteSpace(
|
||||
rover.Descricao
|
||||
)
|
||||
? rover.RoverId
|
||||
: rover.Descricao,
|
||||
gnss?.Latitude,
|
||||
gnss?.Longitude,
|
||||
gnss?.OrientacaoReal,
|
||||
gnss?.LatitudeAnt,
|
||||
gnss?.LongitudeAnt
|
||||
);
|
||||
|
||||
|
||||
if (obj?.Operacao != null)
|
||||
{
|
||||
_entidadeService
|
||||
.AtualizarStatus(
|
||||
_estadoDinamico,
|
||||
rover.RoverId,
|
||||
obj.Operacao.Status
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void AtualizarMovimentoRover(
|
||||
OperacaoParametrosModel rover)
|
||||
{
|
||||
var dados =
|
||||
rover.DadosLeitura;
|
||||
|
||||
|
||||
if (dados == null)
|
||||
return;
|
||||
|
||||
|
||||
/*
|
||||
* =====================================================
|
||||
* RASTRO
|
||||
* =====================================================
|
||||
*/
|
||||
|
||||
double? latitude =
|
||||
dados.Gnss?.Latitude;
|
||||
|
||||
double? longitude =
|
||||
dados.Gnss?.Longitude;
|
||||
|
||||
|
||||
bool coordenadaValida =
|
||||
latitude.HasValue &&
|
||||
longitude.HasValue &&
|
||||
double.IsFinite(
|
||||
latitude.Value
|
||||
) &&
|
||||
double.IsFinite(
|
||||
longitude.Value
|
||||
) &&
|
||||
latitude.Value >= -90.0 &&
|
||||
latitude.Value <= 90.0 &&
|
||||
longitude.Value >= -180.0 &&
|
||||
longitude.Value <= 180.0;
|
||||
|
||||
|
||||
bool pulverizando =
|
||||
dados.Atuador?
|
||||
.Bicos?
|
||||
.Any(
|
||||
x =>
|
||||
x.ComandoEstado ==
|
||||
true
|
||||
)
|
||||
== true;
|
||||
|
||||
|
||||
if (coordenadaValida)
|
||||
{
|
||||
_movimentoService
|
||||
.AtualizarPosicao(
|
||||
_estadoMovimento,
|
||||
rover.RoverId,
|
||||
latitude!.Value,
|
||||
longitude!.Value,
|
||||
pulverizando,
|
||||
distanciaMinimaMetros:
|
||||
0.10,
|
||||
distanciaMaximaSegmentoMetros:
|
||||
20.0
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* =====================================================
|
||||
* MPC
|
||||
* =====================================================
|
||||
*/
|
||||
|
||||
var predicao =
|
||||
dados.Controle?
|
||||
.SimulacaoMPC?
|
||||
.Where(
|
||||
x =>
|
||||
double.IsFinite(
|
||||
x.latitude
|
||||
) &&
|
||||
double.IsFinite(
|
||||
x.longitude
|
||||
) &&
|
||||
x.latitude >= -90.0 &&
|
||||
x.latitude <= 90.0 &&
|
||||
x.longitude >= -180.0 &&
|
||||
x.longitude <= 180.0
|
||||
)
|
||||
.Select(
|
||||
x =>
|
||||
new MapaPontoWgs84(
|
||||
x.latitude,
|
||||
x.longitude
|
||||
)
|
||||
)
|
||||
.ToList();
|
||||
|
||||
|
||||
_movimentoService
|
||||
.DefinirPredicao(
|
||||
_estadoMovimento,
|
||||
rover.RoverId,
|
||||
predicao
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* =========================================================
|
||||
* FOCO
|
||||
* =========================================================
|
||||
*/
|
||||
|
||||
public void DefinirFocoEntidade(
|
||||
string? entidadeId,
|
||||
MapViewControl? mapaAtual = null)
|
||||
{
|
||||
_entidadeService
|
||||
.DefinirFoco(
|
||||
_estadoDinamico,
|
||||
entidadeId
|
||||
);
|
||||
|
||||
|
||||
if (
|
||||
mapaAtual?.Mapa?.Map != null
|
||||
)
|
||||
{
|
||||
RegistrarMapa(
|
||||
mapaAtual
|
||||
);
|
||||
|
||||
|
||||
RenderizarEntidadesDinamicasNoMapa(
|
||||
mapaAtual
|
||||
);
|
||||
|
||||
|
||||
mapaAtual.Mapa.Refresh();
|
||||
}
|
||||
|
||||
|
||||
Log(
|
||||
"[MAPA][FOCO] " +
|
||||
$"Id={(entidadeId ?? "NENHUM")}"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public MapaEntidadeDinamica?
|
||||
ObterEntidade(
|
||||
string? entidadeId)
|
||||
{
|
||||
if (
|
||||
string.IsNullOrWhiteSpace(
|
||||
entidadeId
|
||||
)
|
||||
)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
return _entidadeService
|
||||
.Obter(
|
||||
_estadoDinamico,
|
||||
entidadeId
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* =========================================================
|
||||
* STATUS EXTERNO
|
||||
* =========================================================
|
||||
*/
|
||||
|
||||
public void AtualizarStatusRoverMapa(
|
||||
string? roverId,
|
||||
StatusOperacao status,
|
||||
MapViewControl? mapaAtual = null)
|
||||
{
|
||||
if (
|
||||
string.IsNullOrWhiteSpace(
|
||||
roverId
|
||||
)
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var entidade =
|
||||
_entidadeService
|
||||
.Obter(
|
||||
_estadoDinamico,
|
||||
roverId
|
||||
);
|
||||
|
||||
|
||||
if (entidade == null)
|
||||
return;
|
||||
|
||||
|
||||
_entidadeService
|
||||
.AtualizarStatus(
|
||||
_estadoDinamico,
|
||||
roverId,
|
||||
status
|
||||
);
|
||||
|
||||
|
||||
if (
|
||||
mapaAtual?.Mapa?.Map != null
|
||||
)
|
||||
{
|
||||
RenderizarEntidadesDinamicasNoMapa(
|
||||
mapaAtual
|
||||
);
|
||||
|
||||
|
||||
mapaAtual.Mapa.Refresh();
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (
|
||||
var mapa
|
||||
in _mapasRegistrados
|
||||
.ToArray())
|
||||
{
|
||||
RenderizarEntidadesDinamicasNoMapa(
|
||||
mapa
|
||||
);
|
||||
|
||||
|
||||
mapa.Mapa.Refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* =========================================================
|
||||
* MOVIMENTO
|
||||
* =========================================================
|
||||
*/
|
||||
|
||||
public void LimparMovimentoRover(
|
||||
string? roverId,
|
||||
bool limparPredicao = true)
|
||||
{
|
||||
if (
|
||||
string.IsNullOrWhiteSpace(
|
||||
roverId
|
||||
)
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
_movimentoService
|
||||
.LimparRastro(
|
||||
_estadoMovimento,
|
||||
roverId,
|
||||
limparPredicao
|
||||
);
|
||||
|
||||
|
||||
foreach (
|
||||
var mapa
|
||||
in _mapasRegistrados.ToArray())
|
||||
{
|
||||
RenderizarMovimentoNoMapa(
|
||||
mapa
|
||||
);
|
||||
|
||||
|
||||
GarantirEntidadesNoTopo(
|
||||
mapa
|
||||
);
|
||||
|
||||
|
||||
mapa.Mapa.Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* =========================================================
|
||||
* REFERÊNCIA GNSS
|
||||
* =========================================================
|
||||
*/
|
||||
|
||||
public void DefinirExibicaoReferenciasGnss(
|
||||
bool mostrar)
|
||||
{
|
||||
if (
|
||||
_mostrarReferenciasGnss ==
|
||||
mostrar
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
_mostrarReferenciasGnss =
|
||||
mostrar;
|
||||
|
||||
|
||||
foreach (
|
||||
var mapa
|
||||
in _mapasRegistrados.ToArray())
|
||||
{
|
||||
RenderizarReferenciasGnssNoMapa(
|
||||
mapa
|
||||
);
|
||||
|
||||
|
||||
GarantirEntidadesNoTopo(
|
||||
mapa
|
||||
);
|
||||
|
||||
|
||||
mapa.Mapa.Refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,767 @@
|
|||
using AgroBase.Models;
|
||||
using AgroBase.Models.Operacoes;
|
||||
using Mapsui.Layers;
|
||||
using OperationControl.Controls;
|
||||
using OperationControl.Mapas.Editing;
|
||||
using OperationControl.Mapas.Interaction;
|
||||
using OperationControl.Mapas.Models;
|
||||
using OperationControl.Mapas.Rendering;
|
||||
using OperationControl.Mapas.Services;
|
||||
using static AgroBase.Models.Enums;
|
||||
|
||||
namespace OperationControl.Mapas
|
||||
{
|
||||
public sealed partial class MapaOperacaoController
|
||||
{
|
||||
/*
|
||||
* =========================================================
|
||||
* EVENTOS
|
||||
* =========================================================
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// Informa à aplicação que uma entidade dinâmica
|
||||
/// foi clicada no mapa.
|
||||
///
|
||||
/// O Controller NÃO decide qual Rover deve ser
|
||||
/// selecionado na aplicação.
|
||||
/// </summary>
|
||||
public event Action<string>? EntidadeClicada;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Permite ao Dock encaminhar mensagens do
|
||||
/// subsistema de mapas para o logger da aplicação.
|
||||
/// </summary>
|
||||
public event Action<string>? LogGerado;
|
||||
|
||||
public event Action? EstadoEdicaoAlterado;
|
||||
|
||||
/// <summary>
|
||||
/// Disparado quando algum estado estrutural do mapa
|
||||
/// muda e uma interface externa pode precisar
|
||||
/// atualizar seus indicadores.
|
||||
///
|
||||
/// Exemplos:
|
||||
/// mapa carregado,
|
||||
/// mapa limpo,
|
||||
/// seleção operacional alterada.
|
||||
/// </summary>
|
||||
public event Action? EstadoMapaAlterado;
|
||||
|
||||
public event Action<MapaResultadoValidacao>? ValidacaoEdicaoFalhou;
|
||||
|
||||
|
||||
/*
|
||||
* =========================================================
|
||||
* ESTADOS
|
||||
* =========================================================
|
||||
*/
|
||||
|
||||
private readonly MapaWorkspace _workspace = new();
|
||||
|
||||
private readonly MapaEstadoDinamico _estadoDinamico = new();
|
||||
|
||||
private readonly MapaEstadoMovimento _estadoMovimento = new();
|
||||
|
||||
private readonly MapaEstadoEdicao _estadoEdicao = new();
|
||||
|
||||
private readonly MapaEstadoRecorte _estadoRecorte = new();
|
||||
|
||||
private MapaFerramentaEdicao _ferramentaEdicaoAtual = MapaFerramentaEdicao.Nenhuma;
|
||||
|
||||
|
||||
/*
|
||||
* =========================================================
|
||||
* SERVICES
|
||||
* =========================================================
|
||||
*/
|
||||
|
||||
private readonly MapaImportService _importService = new();
|
||||
private readonly MapaSelectionService _selectionService = new();
|
||||
private readonly MapaExportService _exportService = new();
|
||||
private readonly MapaContratoImportService _contratoImportService = new();
|
||||
private readonly MapaRotaRetornoService _rotaRetornoService = new();
|
||||
private readonly MapaEntidadeDinamicaService _entidadeService = new();
|
||||
private readonly MapaMovimentoService _movimentoService = new();
|
||||
private readonly MapaEdicaoService _edicaoService = new();
|
||||
private readonly MapaDocumentoCloneService _documentoCloneService = new();
|
||||
private readonly MapaEstimativaOperacaoService _estimativaOperacaoService = new();
|
||||
private readonly MapaValidationService _validationService = new();
|
||||
|
||||
/*
|
||||
* =========================================================
|
||||
* RENDERERS
|
||||
* =========================================================
|
||||
*/
|
||||
|
||||
private readonly MapaRenderService _renderService = new();
|
||||
private readonly MapaRotaRetornoRenderService _rotaRenderService = new();
|
||||
private readonly MapaEntidadeDinamicaRenderService _entidadeRenderService = new();
|
||||
private readonly MapaRastroRenderService _rastroRenderService = new();
|
||||
private readonly MapaPredicaoMpcRenderService _predicaoRenderService = new();
|
||||
private readonly MapaReferenciaGnssRenderService _referenciaGnssRenderService = new();
|
||||
private readonly MapaEdicaoRenderService _edicaoRenderService = new();
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* =========================================================
|
||||
* ESTADO DE RENDER POR MAPVIEW
|
||||
* =========================================================
|
||||
*/
|
||||
|
||||
private readonly Dictionary<
|
||||
MapViewControl,
|
||||
MapaRenderResult>
|
||||
_rendersMapa =
|
||||
new();
|
||||
|
||||
|
||||
private readonly Dictionary<
|
||||
MapViewControl,
|
||||
MemoryLayer>
|
||||
_layersRetorno =
|
||||
new();
|
||||
|
||||
|
||||
private readonly Dictionary<
|
||||
MapViewControl,
|
||||
MemoryLayer>
|
||||
_layersEntidades =
|
||||
new();
|
||||
|
||||
|
||||
private readonly Dictionary<
|
||||
MapViewControl,
|
||||
MemoryLayer>
|
||||
_layersRastro =
|
||||
new();
|
||||
|
||||
|
||||
private readonly Dictionary<
|
||||
MapViewControl,
|
||||
MemoryLayer>
|
||||
_layersPredicao =
|
||||
new();
|
||||
|
||||
|
||||
private readonly Dictionary<
|
||||
MapViewControl,
|
||||
MemoryLayer>
|
||||
_layersReferenciaGnss =
|
||||
new();
|
||||
|
||||
|
||||
/*
|
||||
* MapViews que já passaram pelo Controller.
|
||||
*
|
||||
* Não significa que estão visíveis neste instante.
|
||||
*/
|
||||
private readonly HashSet<MapViewControl>
|
||||
_mapasRegistrados =
|
||||
new();
|
||||
|
||||
|
||||
private readonly HashSet<MapViewControl> _mapasEmEdicao = new();
|
||||
|
||||
|
||||
/*
|
||||
* Modo anterior ao início da edição.
|
||||
*
|
||||
* Permite ao Controller restaurar exatamente o contexto
|
||||
* da View quando a sessão de edição for cancelada.
|
||||
*/
|
||||
private readonly Dictionary<
|
||||
MapViewControl,
|
||||
MapaModoInteracao>
|
||||
_modoInteracaoAntesEdicao =
|
||||
new();
|
||||
|
||||
|
||||
/*
|
||||
* =========================================================
|
||||
* PREFERÊNCIAS VISUAIS
|
||||
* =========================================================
|
||||
*/
|
||||
|
||||
private bool
|
||||
_mostrarReferenciasGnss =
|
||||
true;
|
||||
|
||||
|
||||
/*
|
||||
* =========================================================
|
||||
* PROPRIEDADES PÚBLICAS
|
||||
* =========================================================
|
||||
*/
|
||||
|
||||
public bool PossuiDocumentoFonte => _workspace.PossuiDocumentoFonte;
|
||||
|
||||
|
||||
public int QuantidadeRuasFonte => _workspace.QuantidadeRuasFonte;
|
||||
|
||||
public bool PossuiMapa =>
|
||||
_workspace.PossuiMapa;
|
||||
|
||||
|
||||
public MapaDocumento? Documento =>
|
||||
_workspace.Documento;
|
||||
|
||||
|
||||
public TipoMapaOperacao TipoMapa =>
|
||||
_workspace.Documento?.Tipo
|
||||
?? TipoMapaOperacao.Indefinido;
|
||||
|
||||
|
||||
public int QuantidadeRuas =>
|
||||
_workspace.Documento?
|
||||
.QuantidadeRuas
|
||||
?? 0;
|
||||
|
||||
|
||||
public int QuantidadeRuasSelecionadas =>
|
||||
_workspace.Selecao?
|
||||
.Quantidade
|
||||
?? 0;
|
||||
|
||||
|
||||
public bool PossuiPontosRetorno =>
|
||||
_workspace.RotaRetorno?
|
||||
.PossuiPontos
|
||||
?? false;
|
||||
|
||||
|
||||
public int QuantidadePontosRetorno =>
|
||||
_workspace.RotaRetorno?
|
||||
.Quantidade
|
||||
?? 0;
|
||||
|
||||
|
||||
public bool MostrarReferenciasGnss =>
|
||||
_mostrarReferenciasGnss;
|
||||
|
||||
public MapaFerramentaEdicao FerramentaEdicaoAtual => _ferramentaEdicaoAtual;
|
||||
|
||||
|
||||
/*
|
||||
* =========================================================
|
||||
* EDIÇÃO
|
||||
* =========================================================
|
||||
*/
|
||||
|
||||
public bool EdicaoAtiva =>
|
||||
_estadoEdicao.EmEdicao;
|
||||
|
||||
|
||||
public bool EdicaoModificada =>
|
||||
_estadoEdicao.Modificado;
|
||||
|
||||
|
||||
public int QuantidadeRuasSelecionadasEdicao =>
|
||||
_estadoEdicao.QuantidadeSelecionadas;
|
||||
|
||||
|
||||
public bool CapturaBordaGnssAutomaticaAtiva => _estadoRecorte.GravacaoAutomaticaAtiva;
|
||||
|
||||
|
||||
public MapaDocumento? DocumentoEmEdicao => _estadoEdicao.DocumentoTrabalho;
|
||||
|
||||
|
||||
public IReadOnlyCollection<string> RuasSelecionadasEdicao => _estadoEdicao.RuasSelecionadas;
|
||||
|
||||
|
||||
/*
|
||||
* =========================================================
|
||||
* CARREGAMENTO
|
||||
* =========================================================
|
||||
*/
|
||||
|
||||
public MapaDocumento CarregarMapaArquivo(string caminhoArquivo, TipoMapaOperacao tipo)
|
||||
{
|
||||
if (
|
||||
string.IsNullOrWhiteSpace(
|
||||
caminhoArquivo
|
||||
)
|
||||
)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Caminho do mapa inválido.",
|
||||
nameof(caminhoArquivo)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Importa primeiro.
|
||||
*
|
||||
* Se falhar, o Workspace atual permanece intacto.
|
||||
*/
|
||||
var documentoImportado =
|
||||
_importService.Importar(
|
||||
caminhoArquivo,
|
||||
tipo
|
||||
);
|
||||
|
||||
|
||||
/*
|
||||
* O objeto retornado pelo importador é apenas
|
||||
* matéria-prima desta transação.
|
||||
*
|
||||
* Nenhuma das duas cópias persistentes
|
||||
* compartilha estado mutável.
|
||||
*/
|
||||
var documentoFonte =
|
||||
_documentoCloneService
|
||||
.Clonar(
|
||||
documentoImportado
|
||||
);
|
||||
|
||||
|
||||
var documentoAtivo =
|
||||
_documentoCloneService
|
||||
.Clonar(
|
||||
documentoImportado
|
||||
);
|
||||
|
||||
|
||||
/*
|
||||
* Um mapa novo invalida qualquer sessão
|
||||
* de edição anterior.
|
||||
*/
|
||||
CancelarEdicao();
|
||||
|
||||
|
||||
InvalidarRendersWorkspace();
|
||||
InvalidarRendersRotaRetorno();
|
||||
|
||||
|
||||
_workspace.DefinirNovoDocumento(
|
||||
documentoFonte,
|
||||
documentoAtivo
|
||||
);
|
||||
|
||||
EstadoMapaAlterado?.Invoke();
|
||||
|
||||
|
||||
Log(
|
||||
"[MAPA] " +
|
||||
$"Arquivo carregado | " +
|
||||
$"Fonte={documentoFonte.QuantidadeRuas} | " +
|
||||
$"Oficial={documentoAtivo.QuantidadeRuas} | " +
|
||||
$"Nome={documentoAtivo.Nome}"
|
||||
);
|
||||
|
||||
|
||||
return documentoAtivo;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Reconstrói o Workspace a partir do contrato
|
||||
/// operacional recebido do rover.
|
||||
///
|
||||
/// Retorna null quando o rover não possui mapa.
|
||||
/// </summary>
|
||||
public MapaDocumento?
|
||||
CarregarMapaDoRover(
|
||||
OperacaoParametrosModel dados)
|
||||
{
|
||||
if (dados == null)
|
||||
{
|
||||
throw new ArgumentNullException(
|
||||
nameof(dados)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Rover sem mapa.
|
||||
*/
|
||||
if (
|
||||
dados.Mapa?.features?.Any()
|
||||
!= true
|
||||
)
|
||||
{
|
||||
CancelarEdicao();
|
||||
|
||||
|
||||
InvalidarRendersWorkspace();
|
||||
InvalidarRendersRotaRetorno();
|
||||
|
||||
|
||||
_workspace.Limpar();
|
||||
|
||||
EstadoMapaAlterado?.Invoke();
|
||||
|
||||
|
||||
Log(
|
||||
"[MAPA][ROVER] " +
|
||||
"Rover não possui mapa operacional."
|
||||
);
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Toda reconstrução acontece fora
|
||||
* do estado oficial primeiro.
|
||||
*/
|
||||
var documentoImportado =
|
||||
_contratoImportService.Importar(
|
||||
dados.Mapa,
|
||||
dados.TipoMapa,
|
||||
$"Mapa rover {dados.RoverId}"
|
||||
);
|
||||
|
||||
|
||||
var documentoFonte =
|
||||
_documentoCloneService
|
||||
.Clonar(
|
||||
documentoImportado
|
||||
);
|
||||
|
||||
|
||||
var novoDocumento =
|
||||
_documentoCloneService
|
||||
.Clonar(
|
||||
documentoImportado
|
||||
);
|
||||
|
||||
|
||||
var novaSelecao =
|
||||
new MapaSelecaoOperacao();
|
||||
|
||||
|
||||
_selectionService.DefinirSelecao(
|
||||
novoDocumento,
|
||||
novaSelecao,
|
||||
dados.RuasPercorrer
|
||||
?? Enumerable.Empty<string>()
|
||||
);
|
||||
|
||||
|
||||
var novaRotaRetorno =
|
||||
new MapaRotaRetorno();
|
||||
|
||||
|
||||
_rotaRetornoService
|
||||
.DefinirPeloContrato(
|
||||
novaRotaRetorno,
|
||||
dados.PontosRetorno
|
||||
);
|
||||
|
||||
|
||||
/*
|
||||
* Só agora alteramos o estado oficial.
|
||||
*/
|
||||
CancelarEdicao();
|
||||
|
||||
|
||||
InvalidarRendersWorkspace();
|
||||
InvalidarRendersRotaRetorno();
|
||||
|
||||
|
||||
_workspace.DefinirNovoEstado(
|
||||
documentoFonte,
|
||||
novoDocumento,
|
||||
novaSelecao,
|
||||
novaRotaRetorno
|
||||
);
|
||||
|
||||
EstadoMapaAlterado?.Invoke();
|
||||
|
||||
|
||||
Log(
|
||||
"[MAPA][ROVER] " +
|
||||
$"Mapa recebido | " +
|
||||
$"Fonte={documentoFonte.QuantidadeRuas} | " +
|
||||
$"Oficial={novoDocumento.QuantidadeRuas} | " +
|
||||
$"Selecionadas={novaSelecao.Quantidade} | " +
|
||||
$"Percurso=[" +
|
||||
$"{string.Join(" -> ", novaSelecao.RuasOrdenadas)}]"
|
||||
);
|
||||
|
||||
|
||||
return novoDocumento;
|
||||
}
|
||||
|
||||
|
||||
public void LimparMapa()
|
||||
{
|
||||
CancelarEdicao();
|
||||
|
||||
|
||||
InvalidarRendersWorkspace();
|
||||
InvalidarRendersRotaRetorno();
|
||||
|
||||
|
||||
_workspace.Limpar();
|
||||
|
||||
EstadoMapaAlterado?.Invoke();
|
||||
|
||||
|
||||
/*
|
||||
* Os estados de Base/Rover e movimento
|
||||
* NÃO pertencem ao mapa operacional.
|
||||
*
|
||||
* Portanto não são apagados aqui.
|
||||
*/
|
||||
Log(
|
||||
"[MAPA] Workspace limpo."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* =========================================================
|
||||
* CONTRATO OPERACIONAL
|
||||
* =========================================================
|
||||
*/
|
||||
|
||||
public MapaFeatureCollectionModel?
|
||||
CriarMapaOperacional()
|
||||
{
|
||||
if (
|
||||
_workspace.Documento == null
|
||||
)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
return _exportService
|
||||
.CriarMapaOperacional(
|
||||
_workspace.Documento
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public List<string>
|
||||
ObterRuasPercorrer()
|
||||
{
|
||||
if (
|
||||
_workspace.Documento == null
|
||||
)
|
||||
{
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
|
||||
return _selectionService
|
||||
.ObterRuasOrdenadas(
|
||||
_workspace.Selecao
|
||||
)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* =========================================================
|
||||
* SELEÇÃO OPERACIONAL
|
||||
* =========================================================
|
||||
*/
|
||||
|
||||
public bool AlternarRuaOperacao(
|
||||
string ruaId)
|
||||
{
|
||||
if (
|
||||
_workspace.Documento == null
|
||||
)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Não existe mapa operacional carregado."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
bool selecionada =
|
||||
_selectionService
|
||||
.AlternarRua(
|
||||
_workspace.Documento,
|
||||
_workspace.Selecao,
|
||||
ruaId
|
||||
);
|
||||
|
||||
|
||||
AtualizarEstiloSelecaoRua(
|
||||
ruaId
|
||||
);
|
||||
|
||||
EstadoMapaAlterado?.Invoke();
|
||||
|
||||
|
||||
int? ordem =
|
||||
_selectionService
|
||||
.ObterOrdem(
|
||||
_workspace.Selecao,
|
||||
ruaId
|
||||
);
|
||||
|
||||
|
||||
Log(
|
||||
"[MAPA][SELECAO] " +
|
||||
$"Rua={ruaId} | " +
|
||||
$"Selecionada={selecionada} | " +
|
||||
$"Ordem={(ordem?.ToString() ?? "-")}"
|
||||
);
|
||||
|
||||
|
||||
return selecionada;
|
||||
}
|
||||
|
||||
|
||||
public void LimparSelecaoOperacao()
|
||||
{
|
||||
_selectionService.Limpar(
|
||||
_workspace.Selecao
|
||||
);
|
||||
|
||||
|
||||
foreach (
|
||||
var item
|
||||
in _rendersMapa.ToArray())
|
||||
{
|
||||
_renderService
|
||||
.AplicarSelecao(
|
||||
item.Value,
|
||||
_workspace.Selecao
|
||||
);
|
||||
|
||||
|
||||
item.Key.Mapa.Refresh();
|
||||
}
|
||||
|
||||
EstadoMapaAlterado?.Invoke();
|
||||
|
||||
|
||||
Log(
|
||||
"[MAPA][SELECAO] Seleção limpa."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* =========================================================
|
||||
* ROTA DE RETORNO
|
||||
* =========================================================
|
||||
*/
|
||||
|
||||
public bool AlternarPontoRetorno(
|
||||
double latitude,
|
||||
double longitude)
|
||||
{
|
||||
bool adicionado =
|
||||
_rotaRetornoService
|
||||
.AlternarPonto(
|
||||
_workspace.RotaRetorno,
|
||||
latitude,
|
||||
longitude
|
||||
);
|
||||
|
||||
|
||||
RenderizarRotasRegistradas();
|
||||
|
||||
|
||||
Log(
|
||||
"[MAPA][RETORNO] " +
|
||||
$"Lat={latitude:F8} | " +
|
||||
$"Lon={longitude:F8} | " +
|
||||
$"Acao={(adicionado ? "ADICIONADO" : "REMOVIDO")} | " +
|
||||
$"Pontos={QuantidadePontosRetorno}"
|
||||
);
|
||||
|
||||
|
||||
return adicionado;
|
||||
}
|
||||
|
||||
|
||||
public void LimparRotaRetorno()
|
||||
{
|
||||
_rotaRetornoService.Limpar(
|
||||
_workspace.RotaRetorno
|
||||
);
|
||||
|
||||
|
||||
RenderizarRotasRegistradas();
|
||||
|
||||
|
||||
Log(
|
||||
"[MAPA][RETORNO] Rota limpa."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public double
|
||||
ObterDistanciaRotaRetornoMetros()
|
||||
{
|
||||
return _rotaRetornoService
|
||||
.ObterDistanciaTotalMetros(
|
||||
_workspace.RotaRetorno
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public List<double[]>
|
||||
ExportarRotaRetorno()
|
||||
{
|
||||
return _rotaRetornoService
|
||||
.ExportarContrato(
|
||||
_workspace.RotaRetorno
|
||||
)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
|
||||
public MapaEstimativaOperacao CalcularEstimativaOperacao(double? velocidadeSemErvasMs = null, double? velocidadeComErvasMs = null, double percentualErvas = 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
return _estimativaOperacaoService
|
||||
.Calcular(
|
||||
_workspace.Documento,
|
||||
_workspace.Selecao,
|
||||
velocidadeSemErvasMs,
|
||||
velocidadeComErvasMs,
|
||||
percentualErvas
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log(
|
||||
"[MAPA][ESTIMATIVA][ERRO] " +
|
||||
ex.Message
|
||||
);
|
||||
|
||||
|
||||
return new MapaEstimativaOperacao
|
||||
{
|
||||
QuantidadeRuasSelecionadas =
|
||||
_workspace
|
||||
.Selecao
|
||||
.Quantidade,
|
||||
|
||||
MotivoIndisponivel =
|
||||
ex.Message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* =========================================================
|
||||
* LOG
|
||||
* =========================================================
|
||||
*/
|
||||
|
||||
private void Log(
|
||||
string mensagem)
|
||||
{
|
||||
LogGerado?.Invoke(
|
||||
mensagem
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
public enum MapaAcaoBordaGnss
|
||||
{
|
||||
RegistrarPonto = 0,
|
||||
|
||||
RemoverUltimo = 1,
|
||||
|
||||
Finalizar = 2,
|
||||
|
||||
Continuar = 3,
|
||||
|
||||
Descartar = 4,
|
||||
|
||||
Aplicar = 5,
|
||||
|
||||
DefinirModoManual = 6,
|
||||
|
||||
DefinirModoAutomatico = 7,
|
||||
|
||||
IniciarGravacaoAutomatica = 8
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
using OperationControl.Mapas.Models;
|
||||
|
||||
namespace OperationControl.Mapas.Editing
|
||||
{
|
||||
internal sealed class MapaEdicaoSnapshot
|
||||
{
|
||||
public MapaDocumento Documento
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<string>
|
||||
RuasSelecionadas
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
|
||||
public MapaEdicaoSnapshot(
|
||||
MapaDocumento documento,
|
||||
IEnumerable<string> ruasSelecionadas)
|
||||
{
|
||||
Documento =
|
||||
documento
|
||||
?? throw new ArgumentNullException(
|
||||
nameof(documento)
|
||||
);
|
||||
|
||||
|
||||
RuasSelecionadas =
|
||||
new HashSet<string>(
|
||||
ruasSelecionadas
|
||||
?? Array.Empty<string>(),
|
||||
StringComparer.OrdinalIgnoreCase
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal sealed class MapaEdicaoHistoricoItem
|
||||
{
|
||||
public string Descricao
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public MapaEdicaoSnapshot Antes
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public MapaEdicaoSnapshot Depois
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
|
||||
public MapaEdicaoHistoricoItem(
|
||||
string descricao,
|
||||
MapaEdicaoSnapshot antes,
|
||||
MapaEdicaoSnapshot depois)
|
||||
{
|
||||
Descricao =
|
||||
string.IsNullOrWhiteSpace(
|
||||
descricao
|
||||
)
|
||||
? "Alteração no mapa"
|
||||
: descricao.Trim();
|
||||
|
||||
|
||||
Antes =
|
||||
antes
|
||||
?? throw new ArgumentNullException(
|
||||
nameof(antes)
|
||||
);
|
||||
|
||||
|
||||
Depois =
|
||||
depois
|
||||
?? throw new ArgumentNullException(
|
||||
nameof(depois)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,9 @@
|
|||
namespace OperationControl.Mapas.Editing
|
||||
{
|
||||
public enum MapaEscopoRecorte
|
||||
{
|
||||
TodasRuasAtingidas = 0,
|
||||
|
||||
SomenteRuasSelecionadas = 1
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
using OperationControl.Mapas.Models;
|
||||
|
||||
namespace OperationControl.Mapas.Editing
|
||||
{
|
||||
public sealed class MapaEstadoEdicao
|
||||
{
|
||||
private readonly HashSet<string>
|
||||
_ruasSelecionadas =
|
||||
new(
|
||||
StringComparer.OrdinalIgnoreCase
|
||||
);
|
||||
|
||||
|
||||
public MapaDocumento?
|
||||
DocumentoOriginal
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
|
||||
|
||||
public MapaDocumento?
|
||||
DocumentoTrabalho
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
|
||||
|
||||
public IReadOnlyCollection<string>
|
||||
RuasSelecionadas =>
|
||||
_ruasSelecionadas;
|
||||
|
||||
|
||||
public bool EmEdicao =>
|
||||
DocumentoTrabalho != null;
|
||||
|
||||
|
||||
public bool PossuiSelecao =>
|
||||
_ruasSelecionadas.Count > 0;
|
||||
|
||||
|
||||
public int QuantidadeSelecionadas =>
|
||||
_ruasSelecionadas.Count;
|
||||
|
||||
|
||||
public bool Modificado =>
|
||||
DocumentoTrabalho?
|
||||
.Modificado
|
||||
?? false;
|
||||
|
||||
|
||||
internal HashSet<string>
|
||||
RuasSelecionadasMutaveis =>
|
||||
_ruasSelecionadas;
|
||||
|
||||
|
||||
private readonly List<MapaEdicaoHistoricoItem>
|
||||
_historicoDesfazer =
|
||||
new();
|
||||
|
||||
|
||||
private readonly List<MapaEdicaoHistoricoItem>
|
||||
_historicoRefazer =
|
||||
new();
|
||||
|
||||
|
||||
public bool PodeDesfazer =>
|
||||
_historicoDesfazer.Count > 0;
|
||||
|
||||
|
||||
public bool PodeRefazer =>
|
||||
_historicoRefazer.Count > 0;
|
||||
|
||||
|
||||
public int QuantidadeHistoricoDesfazer =>
|
||||
_historicoDesfazer.Count;
|
||||
|
||||
|
||||
public int QuantidadeHistoricoRefazer =>
|
||||
_historicoRefazer.Count;
|
||||
|
||||
|
||||
internal List<MapaEdicaoHistoricoItem>
|
||||
HistoricoDesfazerMutavel =>
|
||||
_historicoDesfazer;
|
||||
|
||||
|
||||
internal List<MapaEdicaoHistoricoItem>
|
||||
HistoricoRefazerMutavel =>
|
||||
_historicoRefazer;
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,341 @@
|
|||
using NetTopologySuite.Geometries;
|
||||
|
||||
namespace OperationControl.Mapas.Editing
|
||||
{
|
||||
public sealed class MapaEstadoRecorte
|
||||
{
|
||||
public double DistanciaMinimaCapturaAutomaticaMetros
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
} = 0.50;
|
||||
|
||||
|
||||
public MapaEscopoRecorte Escopo
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
} =
|
||||
MapaEscopoRecorte
|
||||
.TodasRuasAtingidas;
|
||||
|
||||
|
||||
public bool SomenteSelecionadas =>
|
||||
Escopo ==
|
||||
MapaEscopoRecorte
|
||||
.SomenteRuasSelecionadas;
|
||||
|
||||
|
||||
private readonly List<Coordinate> _pontos = new();
|
||||
|
||||
|
||||
private bool _bordaGnssFinalizada;
|
||||
|
||||
|
||||
public MapaTipoRecorte Tipo
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
} =
|
||||
MapaTipoRecorte.Nenhum;
|
||||
|
||||
|
||||
public MapaModoCapturaGnss ModoCapturaGnss
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
} =
|
||||
MapaModoCapturaGnss.Manual;
|
||||
|
||||
|
||||
public bool CapturaAtiva
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
|
||||
|
||||
public IReadOnlyList<Coordinate> Pontos =>
|
||||
_pontos;
|
||||
|
||||
|
||||
public int QuantidadePontos =>
|
||||
_pontos.Count;
|
||||
|
||||
|
||||
public bool PossuiPontos =>
|
||||
_pontos.Count > 0;
|
||||
|
||||
|
||||
/*
|
||||
* Para polígono precisamos de pelo menos
|
||||
* três vértices distintos.
|
||||
*/
|
||||
public bool PodeConcluirPoligono =>
|
||||
_pontos.Count >= 3;
|
||||
|
||||
|
||||
/*
|
||||
* Para uma borda GNSS aberta precisamos
|
||||
* de pelo menos dois pontos.
|
||||
*/
|
||||
public bool PodeConcluirLinha =>
|
||||
_pontos.Count >= 2;
|
||||
|
||||
|
||||
internal List<Coordinate> PontosMutaveis => _pontos;
|
||||
|
||||
|
||||
public bool BordaGnssFinalizada => Tipo == MapaTipoRecorte.BordaGnss && _bordaGnssFinalizada;
|
||||
|
||||
|
||||
public bool GravacaoAutomaticaAtiva => Tipo == MapaTipoRecorte.BordaGnss && ModoCapturaGnss == MapaModoCapturaGnss.Automatico && CapturaAtiva && !_bordaGnssFinalizada;
|
||||
|
||||
|
||||
internal void AtivarBordaGnssManual()
|
||||
{
|
||||
/*
|
||||
* Primeira entrada na ferramenta.
|
||||
*/
|
||||
if (Tipo != MapaTipoRecorte.BordaGnss)
|
||||
{
|
||||
_pontos.Clear();
|
||||
|
||||
Tipo = MapaTipoRecorte.BordaGnss;
|
||||
|
||||
ModoCapturaGnss = MapaModoCapturaGnss.Manual;
|
||||
|
||||
CapturaAtiva = true;
|
||||
|
||||
_bordaGnssFinalizada = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Se já existia uma borda, preservamos
|
||||
* exatamente o estado.
|
||||
*
|
||||
* Isso permite sair da ferramenta,
|
||||
* selecionar ruas e voltar sem perder
|
||||
* a caminhada.
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
internal void AdicionarPontoBordaGnss(Coordinate ponto)
|
||||
{
|
||||
if (
|
||||
Tipo !=
|
||||
MapaTipoRecorte.BordaGnss ||
|
||||
!CapturaAtiva
|
||||
)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"A captura de borda GNSS não está ativa."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
_pontos.Add(
|
||||
new Coordinate(
|
||||
ponto.X,
|
||||
ponto.Y
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
internal bool RemoverUltimoPontoBordaGnss()
|
||||
{
|
||||
if (
|
||||
Tipo !=
|
||||
MapaTipoRecorte.BordaGnss ||
|
||||
_bordaGnssFinalizada ||
|
||||
_pontos.Count == 0
|
||||
)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Durante gravação automática não deixamos
|
||||
* o usuário disputar com o gravador.
|
||||
*/
|
||||
if (
|
||||
ModoCapturaGnss ==
|
||||
MapaModoCapturaGnss.Automatico &&
|
||||
CapturaAtiva
|
||||
)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
_pontos.RemoveAt(
|
||||
_pontos.Count - 1
|
||||
);
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
internal bool FinalizarBordaGnss()
|
||||
{
|
||||
if (
|
||||
Tipo !=
|
||||
MapaTipoRecorte.BordaGnss ||
|
||||
!CapturaAtiva ||
|
||||
!PodeConcluirLinha
|
||||
)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
CapturaAtiva =
|
||||
false;
|
||||
|
||||
|
||||
_bordaGnssFinalizada =
|
||||
true;
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
internal bool ContinuarBordaGnss()
|
||||
{
|
||||
if (
|
||||
Tipo !=
|
||||
MapaTipoRecorte.BordaGnss ||
|
||||
!_bordaGnssFinalizada
|
||||
)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
_bordaGnssFinalizada =
|
||||
false;
|
||||
|
||||
|
||||
/*
|
||||
* Manual volta imediatamente a aceitar pontos.
|
||||
*
|
||||
* Automático volta para "pronto",
|
||||
* aguardando Iniciar gravação.
|
||||
*/
|
||||
CapturaAtiva =
|
||||
ModoCapturaGnss ==
|
||||
MapaModoCapturaGnss.Manual;
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
internal void DescartarBordaGnss()
|
||||
{
|
||||
_pontos.Clear();
|
||||
|
||||
|
||||
_bordaGnssFinalizada =
|
||||
false;
|
||||
|
||||
|
||||
/*
|
||||
* Manual já fica pronto.
|
||||
* Automático espera Iniciar novamente.
|
||||
*/
|
||||
CapturaAtiva =
|
||||
ModoCapturaGnss ==
|
||||
MapaModoCapturaGnss.Manual;
|
||||
}
|
||||
|
||||
|
||||
internal bool DefinirModoCapturaGnss(MapaModoCapturaGnss modo)
|
||||
{
|
||||
if (
|
||||
Tipo !=
|
||||
MapaTipoRecorte.BordaGnss ||
|
||||
_bordaGnssFinalizada
|
||||
)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (
|
||||
ModoCapturaGnss ==
|
||||
modo
|
||||
)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
ModoCapturaGnss =
|
||||
modo;
|
||||
|
||||
|
||||
/*
|
||||
* MANUAL:
|
||||
* operador já pode registrar pontos.
|
||||
*
|
||||
* AUTOMÁTICO:
|
||||
* espera explicitamente o botão
|
||||
* "Iniciar gravação".
|
||||
*/
|
||||
CapturaAtiva =
|
||||
modo ==
|
||||
MapaModoCapturaGnss.Manual;
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
internal bool IniciarGravacaoAutomatica()
|
||||
{
|
||||
if (
|
||||
Tipo !=
|
||||
MapaTipoRecorte.BordaGnss ||
|
||||
ModoCapturaGnss !=
|
||||
MapaModoCapturaGnss.Automatico ||
|
||||
_bordaGnssFinalizada ||
|
||||
CapturaAtiva
|
||||
)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
CapturaAtiva =
|
||||
true;
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
internal void Limpar()
|
||||
{
|
||||
_pontos.Clear();
|
||||
|
||||
Tipo = MapaTipoRecorte.Nenhum;
|
||||
|
||||
ModoCapturaGnss = MapaModoCapturaGnss.Manual;
|
||||
|
||||
CapturaAtiva = false;
|
||||
|
||||
Escopo = MapaEscopoRecorte.TodasRuasAtingidas;
|
||||
|
||||
_bordaGnssFinalizada = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
namespace OperationControl.Mapas.Editing
|
||||
{
|
||||
/// <summary>
|
||||
/// Ferramenta atualmente ativa dentro
|
||||
/// de uma sessão de edição do mapa.
|
||||
/// </summary>
|
||||
public enum MapaFerramentaEdicao
|
||||
{
|
||||
Nenhuma = 0,
|
||||
|
||||
Selecionar = 1,
|
||||
|
||||
SelecionarRetangulo = 2,
|
||||
|
||||
SelecionarPoligono = 3,
|
||||
|
||||
/*
|
||||
* Contorno livre desenhado com
|
||||
* clique + arraste.
|
||||
*/
|
||||
RecorteLivre = 4,
|
||||
|
||||
/*
|
||||
* Futuro:
|
||||
* polígono formado por cliques.
|
||||
*/
|
||||
RecortePorPontos = 5,
|
||||
|
||||
/*
|
||||
* Futuro:
|
||||
* linha real obtida por GNSS.
|
||||
*/
|
||||
RecorteBordaGnss = 6,
|
||||
|
||||
/*
|
||||
* Alias temporário para qualquer código
|
||||
* antigo que ainda utilize Cortar.
|
||||
*/
|
||||
Cortar = RecorteLivre
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
namespace OperationControl.Mapas.Editing
|
||||
{
|
||||
public enum MapaModoCapturaGnss
|
||||
{
|
||||
Manual = 0,
|
||||
Automatico = 1
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
namespace OperationControl.Mapas.Editing
|
||||
{
|
||||
public sealed class MapaResultadoRecorte
|
||||
{
|
||||
public int RuasAnalisadas
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
|
||||
|
||||
public int RuasIgnoradas
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
|
||||
|
||||
public List<string>
|
||||
RuasAlteradasIds
|
||||
{
|
||||
get;
|
||||
} = new();
|
||||
|
||||
|
||||
public List<string>
|
||||
RuasRemovidasIds
|
||||
{
|
||||
get;
|
||||
} = new();
|
||||
|
||||
|
||||
public List<string>
|
||||
RuasAmbiguasIds
|
||||
{
|
||||
get;
|
||||
} = new();
|
||||
|
||||
|
||||
public int RuasAlteradas =>
|
||||
RuasAlteradasIds.Count;
|
||||
|
||||
|
||||
public int RuasRemovidas =>
|
||||
RuasRemovidasIds.Count;
|
||||
|
||||
|
||||
public int RuasAmbiguas =>
|
||||
RuasAmbiguasIds.Count;
|
||||
|
||||
|
||||
public double
|
||||
ComprimentoRemovidoMetros
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
|
||||
|
||||
public bool HouveAlteracao =>
|
||||
RuasAlteradas > 0 ||
|
||||
RuasRemovidas > 0;
|
||||
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return
|
||||
$"Analisadas={RuasAnalisadas} | " +
|
||||
$"Alteradas={RuasAlteradas} | " +
|
||||
$"Removidas={RuasRemovidas} | " +
|
||||
$"Ignoradas={RuasIgnoradas} | " +
|
||||
$"Ambíguas={RuasAmbiguas} | " +
|
||||
$"Removido=" +
|
||||
$"{ComprimentoRemovidoMetros:F2} m";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
namespace OperationControl.Mapas.Editing
|
||||
{
|
||||
public sealed class MapaResumoBordaGnss
|
||||
{
|
||||
public MapaModoCapturaGnss Modo
|
||||
{
|
||||
get;
|
||||
init;
|
||||
} =
|
||||
MapaModoCapturaGnss.Manual;
|
||||
|
||||
|
||||
public int QuantidadePontos
|
||||
{
|
||||
get;
|
||||
init;
|
||||
}
|
||||
|
||||
|
||||
public double ComprimentoMetros
|
||||
{
|
||||
get;
|
||||
init;
|
||||
}
|
||||
|
||||
|
||||
public bool CapturaAtiva
|
||||
{
|
||||
get;
|
||||
init;
|
||||
}
|
||||
|
||||
|
||||
public bool Finalizada
|
||||
{
|
||||
get;
|
||||
init;
|
||||
}
|
||||
|
||||
|
||||
public bool LeituraGnssValida
|
||||
{
|
||||
get;
|
||||
init;
|
||||
}
|
||||
|
||||
|
||||
public bool QualidadeAlta
|
||||
{
|
||||
get;
|
||||
init;
|
||||
}
|
||||
|
||||
|
||||
public string StatusGnss
|
||||
{
|
||||
get;
|
||||
init;
|
||||
} = "GNSS sem leitura";
|
||||
|
||||
|
||||
public bool GravacaoAutomaticaAtiva
|
||||
{
|
||||
get;
|
||||
init;
|
||||
}
|
||||
|
||||
|
||||
public double DistanciaMinimaAutomaticaMetros
|
||||
{
|
||||
get;
|
||||
init;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
namespace OperationControl.Mapas.Editing
|
||||
{
|
||||
public enum MapaTipoRecorte
|
||||
{
|
||||
Nenhum = 0,
|
||||
|
||||
/*
|
||||
* Mouse pressionado e arrastado.
|
||||
* Ao finalizar, fecha automaticamente
|
||||
* o polígono.
|
||||
*/
|
||||
Livre = 1,
|
||||
|
||||
/*
|
||||
* Vértices inseridos manualmente
|
||||
* por cliques no mapa.
|
||||
* Enter conclui o polígono.
|
||||
*/
|
||||
PorPontos = 2,
|
||||
|
||||
/*
|
||||
* Linha aberta formada por posições GNSS.
|
||||
* Cada rua cruzada é aparada mantendo
|
||||
* o maior fragmento.
|
||||
*/
|
||||
BordaGnss = 3
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,794 @@
|
|||
<UserControl x:Class="OperationControl.Mapas.Editing.Views.MapaEditorOverlay"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Visibility="Collapsed"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Stretch">
|
||||
|
||||
<UserControl.Resources>
|
||||
|
||||
<Style x:Key="ToolButtonStyle" TargetType="ToggleButton">
|
||||
|
||||
<Setter Property="Height"
|
||||
Value="36"/>
|
||||
|
||||
<Setter Property="MinWidth"
|
||||
Value="105"/>
|
||||
|
||||
<Setter Property="Padding"
|
||||
Value="12,5"/>
|
||||
|
||||
<Setter Property="Margin"
|
||||
Value="0,0,8,0"/>
|
||||
|
||||
<Setter Property="Foreground"
|
||||
Value="#D0D0D0"/>
|
||||
|
||||
<Setter Property="Background"
|
||||
Value="#2A2A2A"/>
|
||||
|
||||
<Setter Property="BorderBrush"
|
||||
Value="#505050"/>
|
||||
|
||||
<Setter Property="BorderThickness"
|
||||
Value="1"/>
|
||||
|
||||
<Setter Property="Cursor"
|
||||
Value="Hand"/>
|
||||
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
|
||||
<ControlTemplate TargetType="ToggleButton">
|
||||
|
||||
<Border
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="7">
|
||||
|
||||
<ContentPresenter
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
|
||||
</Border>
|
||||
|
||||
</ControlTemplate>
|
||||
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
|
||||
<Style.Triggers>
|
||||
|
||||
<Trigger Property="IsChecked"
|
||||
Value="True">
|
||||
|
||||
<Setter Property="Background"
|
||||
Value="#27573A"/>
|
||||
|
||||
<Setter Property="BorderBrush"
|
||||
Value="#48A868"/>
|
||||
|
||||
<Setter Property="Foreground"
|
||||
Value="White"/>
|
||||
|
||||
</Trigger>
|
||||
|
||||
<Trigger Property="IsMouseOver"
|
||||
Value="True">
|
||||
|
||||
<Setter Property="BorderBrush"
|
||||
Value="#707070"/>
|
||||
|
||||
</Trigger>
|
||||
|
||||
</Style.Triggers>
|
||||
|
||||
</Style>
|
||||
|
||||
<Style x:Key="EditorButtonStyle" TargetType="Button">
|
||||
|
||||
<Setter Property="Height"
|
||||
Value="36"/>
|
||||
|
||||
<Setter Property="MinWidth"
|
||||
Value="105"/>
|
||||
|
||||
<Setter Property="Padding"
|
||||
Value="12,5"/>
|
||||
|
||||
<Setter Property="Foreground"
|
||||
Value="#D8D8D8"/>
|
||||
|
||||
<Setter Property="Background"
|
||||
Value="#2A2A2A"/>
|
||||
|
||||
<Setter Property="BorderBrush"
|
||||
Value="#505050"/>
|
||||
|
||||
<Setter Property="BorderThickness"
|
||||
Value="1"/>
|
||||
|
||||
<Setter Property="Cursor"
|
||||
Value="Hand"/>
|
||||
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
|
||||
<ControlTemplate TargetType="Button">
|
||||
|
||||
<Border
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="7">
|
||||
|
||||
<ContentPresenter
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
|
||||
</Border>
|
||||
|
||||
</ControlTemplate>
|
||||
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
|
||||
<Style.Triggers>
|
||||
|
||||
<Trigger Property="IsMouseOver"
|
||||
Value="True">
|
||||
|
||||
<Setter Property="Background"
|
||||
Value="#343434"/>
|
||||
|
||||
<Setter Property="BorderBrush"
|
||||
Value="#707070"/>
|
||||
|
||||
</Trigger>
|
||||
|
||||
<Trigger Property="IsEnabled"
|
||||
Value="False">
|
||||
|
||||
<Setter Property="Opacity"
|
||||
Value="0.4"/>
|
||||
|
||||
</Trigger>
|
||||
|
||||
</Style.Triggers>
|
||||
|
||||
</Style>
|
||||
|
||||
<Style x:Key="DangerButtonStyle" TargetType="Button" BasedOn="{StaticResource EditorButtonStyle}">
|
||||
|
||||
<Setter
|
||||
Property="Foreground"
|
||||
Value="#FFB8B8"/>
|
||||
|
||||
<Setter
|
||||
Property="Background"
|
||||
Value="#382020"/>
|
||||
|
||||
<Setter
|
||||
Property="BorderBrush"
|
||||
Value="#704040"/>
|
||||
|
||||
<Style.Triggers>
|
||||
|
||||
<Trigger
|
||||
Property="IsMouseOver"
|
||||
Value="True">
|
||||
|
||||
<Setter
|
||||
Property="Background"
|
||||
Value="#4A2525"/>
|
||||
|
||||
<Setter
|
||||
Property="BorderBrush"
|
||||
Value="#B55050"/>
|
||||
|
||||
<Setter
|
||||
Property="Foreground"
|
||||
Value="White"/>
|
||||
|
||||
</Trigger>
|
||||
|
||||
<Trigger
|
||||
Property="IsEnabled"
|
||||
Value="False">
|
||||
|
||||
<Setter
|
||||
Property="Opacity"
|
||||
Value="0.35"/>
|
||||
|
||||
</Trigger>
|
||||
|
||||
</Style.Triggers>
|
||||
|
||||
</Style>
|
||||
|
||||
<Style x:Key="ApplyButtonStyle" TargetType="Button" BasedOn="{StaticResource EditorButtonStyle}">
|
||||
|
||||
<Setter
|
||||
Property="Foreground"
|
||||
Value="#CFFFE0"/>
|
||||
|
||||
<Setter
|
||||
Property="Background"
|
||||
Value="#183526"/>
|
||||
|
||||
<Setter
|
||||
Property="BorderBrush"
|
||||
Value="#356A49"/>
|
||||
|
||||
<Style.Triggers>
|
||||
|
||||
<Trigger
|
||||
Property="IsMouseOver"
|
||||
Value="True">
|
||||
|
||||
<Setter
|
||||
Property="Background"
|
||||
Value="#204A31"/>
|
||||
|
||||
<Setter
|
||||
Property="BorderBrush"
|
||||
Value="#51A66F"/>
|
||||
|
||||
<Setter
|
||||
Property="Foreground"
|
||||
Value="White"/>
|
||||
|
||||
</Trigger>
|
||||
|
||||
|
||||
<Trigger
|
||||
Property="IsEnabled"
|
||||
Value="False">
|
||||
|
||||
<Setter
|
||||
Property="Opacity"
|
||||
Value="0.35"/>
|
||||
|
||||
</Trigger>
|
||||
|
||||
</Style.Triggers>
|
||||
|
||||
</Style>
|
||||
|
||||
<Style x:Key="ThinScrollBar" TargetType="ScrollBar">
|
||||
<Setter Property="Width" Value="8"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ScrollBar">
|
||||
<Grid Width="8" Background="Transparent">
|
||||
<Border Margin="2,0,2,0" Background="#2F2F2F" CornerRadius="3"/>
|
||||
<Track x:Name="PART_Track" Margin="2,0,2,0" IsDirectionReversed="True">
|
||||
<Track.DecreaseRepeatButton>
|
||||
<RepeatButton Opacity="0" IsHitTestVisible="False"/>
|
||||
</Track.DecreaseRepeatButton>
|
||||
<Track.Thumb>
|
||||
<Thumb>
|
||||
<Thumb.Template>
|
||||
<ControlTemplate TargetType="Thumb">
|
||||
<Border Background="#6B7280" CornerRadius="3" MinHeight="30"/>
|
||||
</ControlTemplate>
|
||||
</Thumb.Template>
|
||||
</Thumb>
|
||||
</Track.Thumb>
|
||||
<Track.IncreaseRepeatButton>
|
||||
<RepeatButton Opacity="0" IsHitTestVisible="False"/>
|
||||
</Track.IncreaseRepeatButton>
|
||||
</Track>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
</UserControl.Resources>
|
||||
|
||||
|
||||
<Border
|
||||
Width="370"
|
||||
Margin="12"
|
||||
Padding="14"
|
||||
VerticalAlignment="Stretch"
|
||||
CornerRadius="10"
|
||||
Background="#EE202020"
|
||||
BorderBrush="#4A4A4A"
|
||||
BorderThickness="1">
|
||||
|
||||
<ScrollViewer
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
CanContentScroll="False"
|
||||
PanningMode="VerticalOnly">
|
||||
|
||||
<ScrollViewer.Resources>
|
||||
<Style
|
||||
TargetType="ScrollBar"
|
||||
BasedOn="{StaticResource ThinScrollBar}" />
|
||||
</ScrollViewer.Resources>
|
||||
|
||||
<StackPanel Margin="0,0,4,0">
|
||||
|
||||
<!-- CABEÇALHO -->
|
||||
<Grid Margin="0,0,0,12">
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel
|
||||
Grid.Column="0"
|
||||
Orientation="Horizontal"
|
||||
VerticalAlignment="Center">
|
||||
|
||||
<TextBlock
|
||||
Text="✎"
|
||||
Foreground="#5FCF80"
|
||||
FontSize="16"
|
||||
Margin="0,0,7,0"/>
|
||||
|
||||
<TextBlock
|
||||
Text="EDITANDO MAPA"
|
||||
Foreground="White"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"/>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
|
||||
<Border
|
||||
Grid.Column="1"
|
||||
Background="#244E32"
|
||||
BorderBrush="#3D8E57"
|
||||
BorderThickness="1"
|
||||
CornerRadius="10"
|
||||
Padding="8,3">
|
||||
|
||||
<TextBlock
|
||||
Text="ATIVO"
|
||||
Foreground="#7DE19A"
|
||||
FontSize="10"
|
||||
FontWeight="Bold"/>
|
||||
|
||||
</Border>
|
||||
|
||||
</Grid>
|
||||
|
||||
<!-- FERRAMENTAS -->
|
||||
<Grid Margin="0,0,0,12">
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="8"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="8"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="8"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="8"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="8"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
|
||||
<ToggleButton
|
||||
x:Name="btnSelecionar"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Content="◎ Selecionar"
|
||||
Margin="0"
|
||||
Style="{StaticResource ToolButtonStyle}"
|
||||
Click="btnSelecionar_Click"/>
|
||||
|
||||
|
||||
<ToggleButton
|
||||
x:Name="btnSelecionarRetangulo"
|
||||
Grid.Row="0"
|
||||
Grid.Column="2"
|
||||
Content="▭ Retângulo"
|
||||
Margin="0"
|
||||
Style="{StaticResource ToolButtonStyle}"
|
||||
Click="btnSelecionarRetangulo_Click"/>
|
||||
|
||||
|
||||
<ToggleButton
|
||||
x:Name="btnRecorteLivre"
|
||||
Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
Content="✂ Livre"
|
||||
Margin="0"
|
||||
Style="{StaticResource ToolButtonStyle}"
|
||||
Click="btnRecorteLivre_Click"/>
|
||||
|
||||
|
||||
<ToggleButton
|
||||
x:Name="btnRecortePorPontos"
|
||||
Grid.Row="2"
|
||||
Grid.Column="2"
|
||||
Content="⬡ Por pontos"
|
||||
Margin="0"
|
||||
Style="{StaticResource ToolButtonStyle}"
|
||||
Click="btnRecortePorPontos_Click"/>
|
||||
|
||||
|
||||
<ToggleButton
|
||||
x:Name="btnRecorteBordaGnss"
|
||||
Grid.Row="4"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="3"
|
||||
Content="📡 Borda GNSS"
|
||||
Margin="0"
|
||||
Style="{StaticResource ToolButtonStyle}"
|
||||
Click="btnRecorteBordaGnss_Click"/>
|
||||
|
||||
|
||||
<Button
|
||||
x:Name="btnLimparSelecao"
|
||||
Grid.Row="6"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="3"
|
||||
Content="Limpar seleção"
|
||||
HorizontalAlignment="Stretch"
|
||||
Style="{StaticResource EditorButtonStyle}"
|
||||
Click="btnLimparSelecao_Click"/>
|
||||
|
||||
</Grid>
|
||||
|
||||
<!-- ESCOPO DO RECORTE -->
|
||||
<Border
|
||||
x:Name="pnlEscopoRecorte"
|
||||
Visibility="Collapsed"
|
||||
Background="#181818"
|
||||
BorderBrush="#363636"
|
||||
BorderThickness="1"
|
||||
CornerRadius="7"
|
||||
Padding="10"
|
||||
Margin="0,0,0,12">
|
||||
|
||||
<StackPanel>
|
||||
|
||||
<TextBlock
|
||||
Text="Aplicar recorte em"
|
||||
Foreground="#CFCFCF"
|
||||
FontSize="11"
|
||||
FontWeight="SemiBold"
|
||||
Margin="0,0,0,7"/>
|
||||
|
||||
|
||||
<RadioButton
|
||||
x:Name="rbRecorteTodas"
|
||||
GroupName="EscopoRecorteMapa"
|
||||
Content="Todas as ruas atingidas"
|
||||
Foreground="#D0D0D0"
|
||||
FontSize="11"
|
||||
Margin="0,0,0,6"
|
||||
IsChecked="True"
|
||||
Checked="rbRecorteTodas_Checked"/>
|
||||
|
||||
|
||||
<RadioButton
|
||||
x:Name="rbRecorteSelecionadas"
|
||||
GroupName="EscopoRecorteMapa"
|
||||
Content="Somente ruas selecionadas"
|
||||
Foreground="#D0D0D0"
|
||||
FontSize="11"
|
||||
Checked="rbRecorteSelecionadas_Checked"/>
|
||||
|
||||
|
||||
<TextBlock
|
||||
x:Name="txtEscopoRecorteAviso"
|
||||
Visibility="Collapsed"
|
||||
Text="⚠ Nenhuma rua está selecionada."
|
||||
Foreground="#F0B45B"
|
||||
FontSize="10"
|
||||
Margin="0,7,0,0"
|
||||
TextWrapping="Wrap"/>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
</Border>
|
||||
|
||||
<!-- BORDA GNSS -->
|
||||
<Border
|
||||
x:Name="pnlBordaGnss"
|
||||
Visibility="Collapsed"
|
||||
Background="#142126"
|
||||
BorderBrush="#315D68"
|
||||
BorderThickness="1"
|
||||
CornerRadius="7"
|
||||
Padding="10"
|
||||
Margin="0,0,0,12">
|
||||
|
||||
<StackPanel>
|
||||
|
||||
<Grid Margin="0,0,0,8">
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
|
||||
<TextBlock
|
||||
Text="📡 Levantamento GNSS"
|
||||
Foreground="#BFEFFF"
|
||||
FontSize="11"
|
||||
FontWeight="SemiBold"/>
|
||||
|
||||
|
||||
<TextBlock
|
||||
x:Name="txtBordaGnssEstado"
|
||||
Grid.Column="1"
|
||||
Text="CAPTURANDO"
|
||||
Foreground="#5FCF80"
|
||||
FontSize="9"
|
||||
FontWeight="Bold"/>
|
||||
|
||||
</Grid>
|
||||
|
||||
|
||||
<StackPanel
|
||||
Orientation="Horizontal"
|
||||
Margin="0,0,0,8">
|
||||
|
||||
<RadioButton
|
||||
x:Name="rbGnssManual"
|
||||
Content="Manual"
|
||||
IsChecked="True"
|
||||
Foreground="#D0D0D0"
|
||||
FontSize="11"
|
||||
Margin="0,0,16,0"
|
||||
Checked="rbGnssManual_Checked"/>
|
||||
|
||||
<RadioButton
|
||||
x:Name="rbGnssAutomatico"
|
||||
Content="Automático"
|
||||
IsEnabled="True"
|
||||
Foreground="#D0D0D0"
|
||||
FontSize="11"
|
||||
Checked="rbGnssAutomatico_Checked"/>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
|
||||
<TextBlock
|
||||
x:Name="txtBordaGnssStatus"
|
||||
Text="GNSS sem leitura."
|
||||
Foreground="#F0B45B"
|
||||
FontSize="10"
|
||||
Margin="0,0,0,5"
|
||||
TextWrapping="Wrap"/>
|
||||
|
||||
|
||||
<TextBlock
|
||||
x:Name="txtBordaGnssResumo"
|
||||
Text="0 pontos • 0,00 m"
|
||||
Foreground="White"
|
||||
FontSize="12"
|
||||
FontWeight="SemiBold"
|
||||
Margin="0,0,0,10"/>
|
||||
|
||||
|
||||
<TextBlock
|
||||
x:Name="txtGnssAutomaticoInfo"
|
||||
Visibility="Collapsed"
|
||||
Text="Registro automático a cada 0,50 m"
|
||||
Foreground="#8EBAC5"
|
||||
FontSize="10"
|
||||
Margin="0,0,0,8"/>
|
||||
|
||||
|
||||
<Grid Margin="0,0,0,8">
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="8"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
|
||||
<Button
|
||||
x:Name="btnGnssRegistrarPonto"
|
||||
Grid.Column="0"
|
||||
Content="+ Registrar ponto"
|
||||
Style="{StaticResource ApplyButtonStyle}"
|
||||
Click="btnGnssRegistrarPonto_Click"/>
|
||||
|
||||
|
||||
<Button
|
||||
x:Name="btnGnssIniciarAutomatico"
|
||||
Grid.Column="0"
|
||||
Visibility="Collapsed"
|
||||
Content="● Iniciar gravação"
|
||||
Style="{StaticResource ApplyButtonStyle}"
|
||||
Click="btnGnssIniciarAutomatico_Click"/>
|
||||
|
||||
|
||||
<Button
|
||||
x:Name="btnGnssRemoverUltimo"
|
||||
Grid.Column="2"
|
||||
Content="↶ Remover último"
|
||||
Style="{StaticResource EditorButtonStyle}"
|
||||
Click="btnGnssRemoverUltimo_Click"/>
|
||||
|
||||
</Grid>
|
||||
|
||||
|
||||
<Grid>
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="8"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
|
||||
<Button
|
||||
x:Name="btnGnssDescartar"
|
||||
Grid.Column="0"
|
||||
Content="✕ Descartar"
|
||||
Style="{StaticResource DangerButtonStyle}"
|
||||
Click="btnGnssDescartar_Click"/>
|
||||
|
||||
|
||||
<Button
|
||||
x:Name="btnGnssFinalizar"
|
||||
Grid.Column="2"
|
||||
Content="✓ Finalizar borda"
|
||||
Style="{StaticResource ApplyButtonStyle}"
|
||||
Click="btnGnssFinalizar_Click"/>
|
||||
|
||||
|
||||
<Button
|
||||
x:Name="btnGnssContinuar"
|
||||
Grid.Column="2"
|
||||
Visibility="Collapsed"
|
||||
Content="✎ Continuar"
|
||||
Style="{StaticResource ApplyButtonStyle}"
|
||||
Click="btnGnssContinuar_Click"/>
|
||||
|
||||
</Grid>
|
||||
|
||||
<Button
|
||||
x:Name="btnGnssAplicarBorda"
|
||||
Visibility="Collapsed"
|
||||
Content="✂ Aplicar borda no mapa"
|
||||
Height="40"
|
||||
Margin="0,8,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Style="{StaticResource ApplyButtonStyle}"
|
||||
Click="btnGnssAplicarBorda_Click"/>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
</Border>
|
||||
|
||||
<!-- HISTÓRICO -->
|
||||
<StackPanel
|
||||
Orientation="Horizontal"
|
||||
Margin="0,0,0,12">
|
||||
|
||||
<Button
|
||||
x:Name="btnDesfazer"
|
||||
Content="↶ Desfazer"
|
||||
Width="160"
|
||||
Margin="0,0,8,0"
|
||||
ToolTip="Ctrl+Z"
|
||||
Style="{StaticResource EditorButtonStyle}"
|
||||
Click="btnDesfazer_Click"/>
|
||||
|
||||
|
||||
<Button
|
||||
x:Name="btnRefazer"
|
||||
Content="↷ Refazer"
|
||||
Width="160"
|
||||
ToolTip="Ctrl+Y"
|
||||
Style="{StaticResource EditorButtonStyle}"
|
||||
Click="btnRefazer_Click"/>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<!-- OPERAÇÕES -->
|
||||
<StackPanel Margin="0,0,0,12">
|
||||
|
||||
<Button
|
||||
x:Name="btnExcluirSelecionadas"
|
||||
Content="🗑 Excluir ruas selecionadas"
|
||||
Height="36"
|
||||
HorizontalAlignment="Stretch"
|
||||
Style="{StaticResource DangerButtonStyle}"
|
||||
ToolTip="Delete"
|
||||
Click="btnExcluirSelecionadas_Click"/>
|
||||
|
||||
<Button
|
||||
x:Name="btnRestaurarMapaFonte"
|
||||
Content="↺ Restaurar mapa original"
|
||||
Height="38"
|
||||
Margin="0,8,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Style="{StaticResource EditorButtonStyle}"
|
||||
Foreground="#FFD77A"
|
||||
Background="#352F20"
|
||||
BorderBrush="#78642E"
|
||||
ToolTip="Restaura no editor o mapa que entrou originalmente no sistema. Ctrl+Z desfaz."
|
||||
Click="btnRestaurarMapaFonte_Click"/>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<!-- STATUS -->
|
||||
<Border
|
||||
Background="#181818"
|
||||
BorderBrush="#363636"
|
||||
BorderThickness="1"
|
||||
CornerRadius="7"
|
||||
Padding="10">
|
||||
|
||||
<StackPanel>
|
||||
|
||||
<TextBlock
|
||||
x:Name="txtSelecionadas"
|
||||
Text="0 ruas selecionadas"
|
||||
Foreground="#FF70CA"
|
||||
FontSize="12"
|
||||
FontWeight="SemiBold"/>
|
||||
|
||||
|
||||
<TextBlock
|
||||
x:Name="txtInstrucao"
|
||||
Text="Clique nas ruas que deseja editar."
|
||||
Foreground="#AFAFAF"
|
||||
FontSize="11"
|
||||
Margin="0,5,0,0"
|
||||
TextWrapping="Wrap"/>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
</Border>
|
||||
|
||||
<!-- FINALIZAÇÃO DA EDIÇÃO -->
|
||||
<Grid Margin="0,14,0,0">
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="8"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
|
||||
<Button
|
||||
x:Name="btnCancelarEdicao"
|
||||
Grid.Column="0"
|
||||
Content="✕ Cancelar"
|
||||
Height="38"
|
||||
Style="{StaticResource EditorButtonStyle}"
|
||||
Click="btnCancelarEdicao_Click"/>
|
||||
|
||||
|
||||
<Button
|
||||
x:Name="btnAplicarEdicao"
|
||||
Grid.Column="2"
|
||||
Content="✓ Aplicar alterações"
|
||||
Height="38"
|
||||
ToolTip="Ctrl+Enter"
|
||||
Style="{StaticResource ApplyButtonStyle}"
|
||||
Click="btnAplicarEdicao_Click"/>
|
||||
|
||||
</Grid>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
</ScrollViewer>
|
||||
|
||||
</Border>
|
||||
|
||||
</UserControl>
|
||||
|
|
@ -0,0 +1,517 @@
|
|||
using System.Windows;
|
||||
using UserControl = System.Windows.Controls.UserControl;
|
||||
|
||||
namespace OperationControl.Mapas.Editing.Views
|
||||
{
|
||||
public partial class MapaEditorOverlay : UserControl
|
||||
{
|
||||
public event Action<MapaFerramentaEdicao>? FerramentaSelecionada;
|
||||
public event Action? LimparSelecaoSolicitado;
|
||||
public event Action? DesfazerSolicitado;
|
||||
public event Action? RefazerSolicitado;
|
||||
public event Action? ExcluirSelecionadasSolicitado;
|
||||
public event Action? AplicarEdicaoSolicitado;
|
||||
public event Action? CancelarEdicaoSolicitado;
|
||||
public event Action<MapaEscopoRecorte>? EscopoRecorteAlterado;
|
||||
public event Action<MapaAcaoBordaGnss>? BordaGnssAcaoSolicitada;
|
||||
public event Action? RestaurarMapaFonteSolicitado;
|
||||
|
||||
private bool _atualizandoEstado;
|
||||
|
||||
|
||||
public MapaEditorOverlay()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
|
||||
public void DefinirEstado(
|
||||
bool ativo,
|
||||
MapaFerramentaEdicao ferramenta,
|
||||
int quantidadeSelecionadas,
|
||||
bool modificado,
|
||||
bool podeDesfazer,
|
||||
bool podeRefazer,
|
||||
MapaEscopoRecorte escopoRecorte = MapaEscopoRecorte.TodasRuasAtingidas,
|
||||
MapaResumoBordaGnss? bordaGnss = null,
|
||||
bool possuiMapaFonte = false)
|
||||
{
|
||||
Visibility = ativo ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
if (!ativo)
|
||||
return;
|
||||
|
||||
_atualizandoEstado = true;
|
||||
|
||||
try
|
||||
{
|
||||
bool ferramentaRecorte =
|
||||
ferramenta == MapaFerramentaEdicao.RecorteLivre ||
|
||||
ferramenta == MapaFerramentaEdicao.RecortePorPontos ||
|
||||
ferramenta == MapaFerramentaEdicao.RecorteBordaGnss;
|
||||
|
||||
|
||||
pnlEscopoRecorte.Visibility =
|
||||
ferramentaRecorte
|
||||
? Visibility.Visible
|
||||
: Visibility.Collapsed;
|
||||
|
||||
|
||||
rbRecorteTodas.IsChecked = escopoRecorte == MapaEscopoRecorte.TodasRuasAtingidas;
|
||||
rbRecorteSelecionadas.IsChecked = escopoRecorte == MapaEscopoRecorte.SomenteRuasSelecionadas;
|
||||
|
||||
bool avisarSemSelecao =
|
||||
ferramentaRecorte &&
|
||||
escopoRecorte == MapaEscopoRecorte.SomenteRuasSelecionadas &&
|
||||
quantidadeSelecionadas <= 0;
|
||||
|
||||
txtEscopoRecorteAviso.Visibility =
|
||||
avisarSemSelecao
|
||||
? Visibility.Visible
|
||||
: Visibility.Collapsed;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_atualizandoEstado = false;
|
||||
}
|
||||
|
||||
btnDesfazer.IsEnabled = podeDesfazer;
|
||||
|
||||
btnRefazer.IsEnabled = podeRefazer;
|
||||
|
||||
btnSelecionar.IsChecked = ferramenta == MapaFerramentaEdicao.Selecionar;
|
||||
|
||||
btnSelecionarRetangulo.IsChecked = ferramenta == MapaFerramentaEdicao.SelecionarRetangulo;
|
||||
|
||||
btnRecorteLivre.IsChecked = ferramenta == MapaFerramentaEdicao.RecorteLivre;
|
||||
|
||||
btnRecortePorPontos.IsChecked = ferramenta == MapaFerramentaEdicao.RecortePorPontos;
|
||||
|
||||
btnExcluirSelecionadas.IsEnabled = quantidadeSelecionadas > 0;
|
||||
|
||||
btnLimparSelecao.IsEnabled = quantidadeSelecionadas > 0;
|
||||
|
||||
btnAplicarEdicao.IsEnabled = modificado;
|
||||
|
||||
btnCancelarEdicao.IsEnabled = ativo;
|
||||
|
||||
btnRecorteBordaGnss.IsChecked = ferramenta == MapaFerramentaEdicao.RecorteBordaGnss;
|
||||
|
||||
btnRestaurarMapaFonte.IsEnabled = possuiMapaFonte;
|
||||
|
||||
bool exibindoBordaGnss = ferramenta == MapaFerramentaEdicao.RecorteBordaGnss;
|
||||
pnlBordaGnss.Visibility = exibindoBordaGnss ? Visibility.Visible : Visibility.Collapsed;
|
||||
if (exibindoBordaGnss && bordaGnss != null)
|
||||
{
|
||||
rbGnssManual.IsChecked = bordaGnss.Modo == MapaModoCapturaGnss.Manual;
|
||||
|
||||
bool automatico = bordaGnss.Modo == MapaModoCapturaGnss.Automatico;
|
||||
bool gravandoAutomatico = bordaGnss.GravacaoAutomaticaAtiva;
|
||||
|
||||
rbGnssManual.IsChecked = !automatico;
|
||||
rbGnssAutomatico.IsChecked = automatico;
|
||||
|
||||
|
||||
/*
|
||||
* Não permitimos trocar de modo
|
||||
* no meio de uma gravação ou depois
|
||||
* da borda finalizada.
|
||||
*/
|
||||
bool podeTrocarModo =
|
||||
!bordaGnss.Finalizada &&
|
||||
!gravandoAutomatico;
|
||||
|
||||
rbGnssManual.IsEnabled = podeTrocarModo;
|
||||
rbGnssAutomatico.IsEnabled = podeTrocarModo;
|
||||
|
||||
btnGnssRegistrarPonto.Visibility =
|
||||
!automatico &&
|
||||
!bordaGnss.Finalizada
|
||||
? Visibility.Visible
|
||||
: Visibility.Collapsed;
|
||||
|
||||
|
||||
btnGnssRegistrarPonto.IsEnabled =
|
||||
!automatico &&
|
||||
bordaGnss.CapturaAtiva &&
|
||||
bordaGnss.LeituraGnssValida;
|
||||
|
||||
|
||||
btnGnssIniciarAutomatico.Visibility =
|
||||
automatico &&
|
||||
!bordaGnss.Finalizada &&
|
||||
!gravandoAutomatico
|
||||
? Visibility.Visible
|
||||
: Visibility.Collapsed;
|
||||
|
||||
|
||||
btnGnssIniciarAutomatico.IsEnabled =
|
||||
automatico &&
|
||||
!bordaGnss.Finalizada &&
|
||||
!gravandoAutomatico &&
|
||||
bordaGnss.LeituraGnssValida;
|
||||
|
||||
|
||||
btnGnssRemoverUltimo.IsEnabled =
|
||||
!bordaGnss.Finalizada &&
|
||||
bordaGnss.QuantidadePontos > 0 &&
|
||||
!gravandoAutomatico;
|
||||
|
||||
|
||||
bool podeFinalizar =
|
||||
bordaGnss.QuantidadePontos >= 2 &&
|
||||
(
|
||||
!automatico
|
||||
||
|
||||
gravandoAutomatico
|
||||
);
|
||||
|
||||
btnGnssFinalizar.IsEnabled = podeFinalizar;
|
||||
|
||||
btnGnssFinalizar.Visibility =
|
||||
bordaGnss.Finalizada
|
||||
? Visibility.Collapsed
|
||||
: (
|
||||
automatico &&
|
||||
!gravandoAutomatico
|
||||
? Visibility.Collapsed
|
||||
: Visibility.Visible
|
||||
);
|
||||
|
||||
|
||||
btnGnssFinalizar.Content =
|
||||
automatico
|
||||
? "■ Finalizar gravação"
|
||||
: "✓ Finalizar borda";
|
||||
|
||||
|
||||
txtBordaGnssResumo.Text =
|
||||
$"{bordaGnss.QuantidadePontos} " +
|
||||
$"{(bordaGnss.QuantidadePontos == 1 ? "ponto" : "pontos")} " +
|
||||
$"• {bordaGnss.ComprimentoMetros:F2} m";
|
||||
|
||||
|
||||
txtGnssAutomaticoInfo.Visibility =
|
||||
automatico
|
||||
? Visibility.Visible
|
||||
: Visibility.Collapsed;
|
||||
|
||||
|
||||
txtGnssAutomaticoInfo.Text =
|
||||
$"Registro automático a cada " +
|
||||
$"{bordaGnss.DistanciaMinimaAutomaticaMetros:F2} m";
|
||||
|
||||
|
||||
txtBordaGnssEstado.Text =
|
||||
bordaGnss.Finalizada
|
||||
? "PRONTA"
|
||||
: automatico
|
||||
? (
|
||||
gravandoAutomatico
|
||||
? "GRAVANDO"
|
||||
: "PRONTO"
|
||||
)
|
||||
: "CAPTURANDO";
|
||||
|
||||
|
||||
txtBordaGnssStatus.Text =
|
||||
bordaGnss.StatusGnss;
|
||||
|
||||
|
||||
txtBordaGnssStatus.Foreground =
|
||||
bordaGnss.LeituraGnssValida
|
||||
? (
|
||||
bordaGnss.QualidadeAlta
|
||||
? new System.Windows.Media
|
||||
.SolidColorBrush(
|
||||
System.Windows.Media
|
||||
.Color
|
||||
.FromRgb(
|
||||
95,
|
||||
207,
|
||||
128
|
||||
)
|
||||
)
|
||||
: new System.Windows.Media
|
||||
.SolidColorBrush(
|
||||
System.Windows.Media
|
||||
.Color
|
||||
.FromRgb(
|
||||
240,
|
||||
180,
|
||||
91
|
||||
)
|
||||
)
|
||||
)
|
||||
: new System.Windows.Media
|
||||
.SolidColorBrush(
|
||||
System.Windows.Media
|
||||
.Color
|
||||
.FromRgb(
|
||||
240,
|
||||
100,
|
||||
100
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
txtBordaGnssEstado.Text =
|
||||
bordaGnss.Finalizada
|
||||
? "PRONTA"
|
||||
: "CAPTURANDO";
|
||||
|
||||
|
||||
txtBordaGnssEstado.Foreground =
|
||||
bordaGnss.Finalizada
|
||||
? System.Windows.Media.Brushes
|
||||
.DeepSkyBlue
|
||||
: new System.Windows.Media
|
||||
.SolidColorBrush(
|
||||
System.Windows.Media
|
||||
.Color
|
||||
.FromRgb(
|
||||
95,
|
||||
207,
|
||||
128
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
btnGnssRegistrarPonto.IsEnabled =
|
||||
bordaGnss.CapturaAtiva &&
|
||||
bordaGnss.LeituraGnssValida;
|
||||
|
||||
|
||||
btnGnssRemoverUltimo.IsEnabled =
|
||||
bordaGnss.CapturaAtiva &&
|
||||
bordaGnss.QuantidadePontos > 0;
|
||||
|
||||
|
||||
btnGnssFinalizar.IsEnabled =
|
||||
bordaGnss.CapturaAtiva &&
|
||||
bordaGnss.QuantidadePontos >= 2;
|
||||
|
||||
|
||||
btnGnssFinalizar.Visibility =
|
||||
bordaGnss.Finalizada
|
||||
? Visibility.Collapsed
|
||||
: Visibility.Visible;
|
||||
|
||||
|
||||
btnGnssContinuar.Visibility =
|
||||
bordaGnss.Finalizada
|
||||
? Visibility.Visible
|
||||
: Visibility.Collapsed;
|
||||
|
||||
|
||||
btnGnssDescartar.IsEnabled =
|
||||
bordaGnss.QuantidadePontos > 0;
|
||||
|
||||
|
||||
bool escopoBordaValido =
|
||||
escopoRecorte !=
|
||||
MapaEscopoRecorte
|
||||
.SomenteRuasSelecionadas
|
||||
||
|
||||
quantidadeSelecionadas > 0;
|
||||
|
||||
|
||||
btnGnssAplicarBorda.Visibility =
|
||||
bordaGnss.Finalizada
|
||||
? Visibility.Visible
|
||||
: Visibility.Collapsed;
|
||||
|
||||
|
||||
btnGnssAplicarBorda.IsEnabled =
|
||||
bordaGnss.Finalizada &&
|
||||
bordaGnss.QuantidadePontos >= 2 &&
|
||||
escopoBordaValido;
|
||||
}
|
||||
|
||||
|
||||
txtSelecionadas.Text =
|
||||
quantidadeSelecionadas switch
|
||||
{
|
||||
0 => "Nenhuma rua selecionada",
|
||||
1 => "1 rua selecionada",
|
||||
_ => $"{quantidadeSelecionadas} ruas selecionadas"
|
||||
};
|
||||
|
||||
txtInstrucao.Text =
|
||||
ferramenta switch
|
||||
{
|
||||
MapaFerramentaEdicao.Selecionar =>
|
||||
"Clique nas ruas que deseja editar.",
|
||||
|
||||
MapaFerramentaEdicao.SelecionarRetangulo =>
|
||||
"Arraste para selecionar ruas. Ctrl + arraste para mover o mapa.",
|
||||
|
||||
MapaFerramentaEdicao.RecorteLivre =>
|
||||
"Arraste contornando a área a remover. Ctrl + arraste move o mapa.",
|
||||
|
||||
MapaFerramentaEdicao.SelecionarPoligono =>
|
||||
"Defina os vértices da área desejada.",
|
||||
|
||||
MapaFerramentaEdicao.RecortePorPontos =>
|
||||
"Clique para marcar os vértices. Enter ou duplo clique conclui. Backspace remove o último ponto. Esc descarta o traçado.",
|
||||
|
||||
MapaFerramentaEdicao.RecorteBordaGnss =>
|
||||
"Registre a borda real usando GNSS.",
|
||||
|
||||
_ =>
|
||||
"Selecione uma ferramenta."
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private void btnSelecionar_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
FerramentaSelecionada?.Invoke(MapaFerramentaEdicao.Selecionar);
|
||||
}
|
||||
|
||||
|
||||
private void btnSelecionarRetangulo_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
FerramentaSelecionada?.Invoke(MapaFerramentaEdicao.SelecionarRetangulo);
|
||||
}
|
||||
|
||||
|
||||
private void btnRecorteLivre_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
FerramentaSelecionada?.Invoke(MapaFerramentaEdicao.RecorteLivre);
|
||||
}
|
||||
|
||||
|
||||
private void btnRecortePorPontos_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
FerramentaSelecionada?.Invoke(MapaFerramentaEdicao.RecortePorPontos);
|
||||
}
|
||||
|
||||
|
||||
private void btnLimparSelecao_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
LimparSelecaoSolicitado?.Invoke();
|
||||
}
|
||||
|
||||
|
||||
private void btnDesfazer_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
DesfazerSolicitado?.Invoke();
|
||||
}
|
||||
|
||||
|
||||
private void btnRefazer_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
RefazerSolicitado?.Invoke();
|
||||
}
|
||||
|
||||
|
||||
private void btnExcluirSelecionadas_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ExcluirSelecionadasSolicitado?.Invoke();
|
||||
}
|
||||
|
||||
|
||||
private void rbRecorteTodas_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_atualizandoEstado)
|
||||
return;
|
||||
|
||||
EscopoRecorteAlterado?.Invoke(MapaEscopoRecorte.TodasRuasAtingidas);
|
||||
}
|
||||
|
||||
|
||||
private void rbRecorteSelecionadas_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_atualizandoEstado)
|
||||
return;
|
||||
|
||||
EscopoRecorteAlterado?.Invoke(MapaEscopoRecorte.SomenteRuasSelecionadas);
|
||||
}
|
||||
|
||||
|
||||
private void btnRecorteBordaGnss_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
FerramentaSelecionada?.Invoke(MapaFerramentaEdicao.RecorteBordaGnss);
|
||||
}
|
||||
|
||||
|
||||
private void btnGnssRegistrarPonto_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
BordaGnssAcaoSolicitada?.Invoke(MapaAcaoBordaGnss.RegistrarPonto);
|
||||
}
|
||||
|
||||
|
||||
private void btnGnssRemoverUltimo_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
BordaGnssAcaoSolicitada?.Invoke(MapaAcaoBordaGnss.RemoverUltimo);
|
||||
}
|
||||
|
||||
|
||||
private void btnGnssFinalizar_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
BordaGnssAcaoSolicitada?.Invoke(MapaAcaoBordaGnss.Finalizar);
|
||||
}
|
||||
|
||||
|
||||
private void btnGnssContinuar_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
BordaGnssAcaoSolicitada?.Invoke(MapaAcaoBordaGnss.Continuar);
|
||||
}
|
||||
|
||||
|
||||
private void btnGnssDescartar_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
BordaGnssAcaoSolicitada?.Invoke(MapaAcaoBordaGnss.Descartar);
|
||||
}
|
||||
|
||||
|
||||
private void btnGnssAplicarBorda_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
BordaGnssAcaoSolicitada?.Invoke(MapaAcaoBordaGnss.Aplicar);
|
||||
}
|
||||
|
||||
|
||||
private void btnAplicarEdicao_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
AplicarEdicaoSolicitado?.Invoke();
|
||||
}
|
||||
|
||||
|
||||
private void btnCancelarEdicao_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
CancelarEdicaoSolicitado?.Invoke();
|
||||
}
|
||||
|
||||
|
||||
private void btnRestaurarMapaFonte_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
RestaurarMapaFonteSolicitado?.Invoke();
|
||||
}
|
||||
|
||||
|
||||
private void rbGnssManual_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_atualizandoEstado)
|
||||
return;
|
||||
|
||||
BordaGnssAcaoSolicitada?.Invoke(MapaAcaoBordaGnss.DefinirModoManual);
|
||||
}
|
||||
|
||||
|
||||
private void rbGnssAutomatico_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_atualizandoEstado)
|
||||
return;
|
||||
|
||||
BordaGnssAcaoSolicitada?.Invoke(MapaAcaoBordaGnss.DefinirModoAutomatico);
|
||||
}
|
||||
|
||||
|
||||
private void btnGnssIniciarAutomatico_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
BordaGnssAcaoSolicitada?.Invoke(MapaAcaoBordaGnss.IniciarGravacaoAutomatica);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
namespace OperationControl.Mapas.Interaction
|
||||
{
|
||||
/// <summary>
|
||||
/// Define como o MapViewControl interpreta a interação do operador.
|
||||
///
|
||||
/// O controle não decide sozinho em qual modo deve estar.
|
||||
/// Essa decisão pertence à aplicação/Controller.
|
||||
/// </summary>
|
||||
public enum MapaModoInteracao
|
||||
{
|
||||
Visualizacao = 0,
|
||||
SelecaoEntidade = 1,
|
||||
SelecaoOperacao = 2,
|
||||
RetornoManual = 3,
|
||||
Edicao = 4
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
namespace OperationControl.Mapas.Interaction
|
||||
{
|
||||
public readonly struct MapaPontoWgs84
|
||||
{
|
||||
public double Latitude
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public double Longitude
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
|
||||
public MapaPontoWgs84(
|
||||
double latitude,
|
||||
double longitude)
|
||||
{
|
||||
if (
|
||||
!double.IsFinite(latitude) ||
|
||||
!double.IsFinite(longitude) ||
|
||||
latitude < -90 ||
|
||||
latitude > 90 ||
|
||||
longitude < -180 ||
|
||||
longitude > 180
|
||||
)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(latitude),
|
||||
"Coordenada WGS84 inválida."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Latitude =
|
||||
latitude;
|
||||
|
||||
Longitude =
|
||||
longitude;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public sealed class MapaPoligonoRecorte
|
||||
{
|
||||
public IReadOnlyList<MapaPontoWgs84>
|
||||
Pontos
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
|
||||
public int QuantidadePontos =>
|
||||
Pontos.Count;
|
||||
|
||||
|
||||
public MapaPoligonoRecorte(
|
||||
IEnumerable<MapaPontoWgs84> pontos)
|
||||
{
|
||||
if (pontos == null)
|
||||
{
|
||||
throw new ArgumentNullException(
|
||||
nameof(pontos)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
var lista =
|
||||
pontos.ToList();
|
||||
|
||||
|
||||
if (lista.Count < 3)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"O polígono precisa de pelo menos 3 pontos.",
|
||||
nameof(pontos)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Pontos =
|
||||
lista;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
namespace OperationControl.Mapas.Interaction
|
||||
{
|
||||
/// <summary>
|
||||
/// Retângulo geográfico produzido por uma
|
||||
/// interação do operador no mapa.
|
||||
///
|
||||
/// Coordenadas em WGS84.
|
||||
/// </summary>
|
||||
public sealed class MapaRetanguloSelecao
|
||||
{
|
||||
public double MinLatitude { get; }
|
||||
|
||||
public double MinLongitude { get; }
|
||||
|
||||
public double MaxLatitude { get; }
|
||||
|
||||
public double MaxLongitude { get; }
|
||||
|
||||
|
||||
public MapaRetanguloSelecao(
|
||||
double minLatitude,
|
||||
double minLongitude,
|
||||
double maxLatitude,
|
||||
double maxLongitude)
|
||||
{
|
||||
MinLatitude =
|
||||
Math.Min(
|
||||
minLatitude,
|
||||
maxLatitude
|
||||
);
|
||||
|
||||
MaxLatitude =
|
||||
Math.Max(
|
||||
minLatitude,
|
||||
maxLatitude
|
||||
);
|
||||
|
||||
MinLongitude =
|
||||
Math.Min(
|
||||
minLongitude,
|
||||
maxLongitude
|
||||
);
|
||||
|
||||
MaxLongitude =
|
||||
Math.Max(
|
||||
minLongitude,
|
||||
maxLongitude
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,171 @@
|
|||
using OperationControl.Mapas.Models;
|
||||
|
||||
namespace OperationControl.Mapas
|
||||
{
|
||||
/// <summary>
|
||||
/// Representa o contexto de trabalho atual do módulo de mapas.
|
||||
///
|
||||
/// Centraliza o mapa carregado e o estado operacional associado
|
||||
/// a ele, sem depender de Mapsui, WPF ou qualquer tela.
|
||||
/// </summary>
|
||||
public sealed class MapaWorkspace
|
||||
{
|
||||
/// <summary>
|
||||
/// Cópia canônica e imutável, por convenção,
|
||||
/// do mapa no instante em que ele entrou
|
||||
/// no subsistema.
|
||||
///
|
||||
/// Não é alterado por Apply, edição,
|
||||
/// Undo/Redo ou alterações operacionais.
|
||||
/// </summary>
|
||||
public MapaDocumento? DocumentoFonte { get; private set; }
|
||||
|
||||
public MapaDocumento? Documento { get; private set; }
|
||||
|
||||
public MapaSelecaoOperacao Selecao { get; private set; } = new();
|
||||
|
||||
public MapaRotaRetorno RotaRetorno { get; private set; } = new();
|
||||
|
||||
|
||||
public bool PossuiDocumentoFonte => DocumentoFonte != null && DocumentoFonte.PossuiRuas;
|
||||
|
||||
|
||||
public int QuantidadeRuasFonte => DocumentoFonte?.QuantidadeRuas ?? 0;
|
||||
|
||||
|
||||
public bool PossuiMapa => Documento != null && Documento.PossuiRuas;
|
||||
|
||||
|
||||
public bool PossuiSelecao => Selecao.PossuiSelecao;
|
||||
|
||||
|
||||
public int QuantidadeRuas => Documento?.QuantidadeRuas ?? 0;
|
||||
|
||||
|
||||
public int QuantidadeSelecionadas => Selecao.Quantidade;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Define um mapa completamente novo.
|
||||
///
|
||||
/// Fonte e documento ativo DEVEM ser
|
||||
/// cópias independentes.
|
||||
/// </summary>
|
||||
public void DefinirNovoDocumento(MapaDocumento documentoFonte, MapaDocumento documentoAtivo)
|
||||
{
|
||||
DefinirNovoEstado(
|
||||
documentoFonte,
|
||||
documentoAtivo,
|
||||
new MapaSelecaoOperacao(),
|
||||
new MapaRotaRetorno()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public void DefinirNovoEstado(MapaDocumento documentoFonte, MapaDocumento documentoAtivo, MapaSelecaoOperacao selecao, MapaRotaRetorno rotaRetorno)
|
||||
{
|
||||
if (documentoFonte == null)
|
||||
{
|
||||
throw new ArgumentNullException(
|
||||
nameof(documentoFonte)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (documentoAtivo == null)
|
||||
{
|
||||
throw new ArgumentNullException(
|
||||
nameof(documentoAtivo)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Essa igualdade seria um bug arquitetural:
|
||||
*
|
||||
* editar Documento significaria editar
|
||||
* também nosso backup.
|
||||
*/
|
||||
if (
|
||||
ReferenceEquals(
|
||||
documentoFonte,
|
||||
documentoAtivo
|
||||
)
|
||||
)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"DocumentoFonte e Documento ativo " +
|
||||
"devem ser instâncias independentes."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
DocumentoFonte =
|
||||
documentoFonte;
|
||||
|
||||
|
||||
DefinirEstado(
|
||||
documentoAtivo,
|
||||
selecao,
|
||||
rotaRetorno
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Substitui somente o documento oficial ativo.
|
||||
///
|
||||
/// DocumentoFonte não é alterado.
|
||||
///
|
||||
/// Útil para fluxos que já possuem uma fonte
|
||||
/// previamente registrada.
|
||||
/// </summary>
|
||||
public void DefinirDocumento(MapaDocumento documento)
|
||||
{
|
||||
DefinirEstado(
|
||||
documento,
|
||||
new MapaSelecaoOperacao(),
|
||||
new MapaRotaRetorno()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Remove o mapa atualmente carregado
|
||||
/// e todo o estado operacional associado.
|
||||
/// </summary>
|
||||
public void Limpar()
|
||||
{
|
||||
DocumentoFonte = null;
|
||||
|
||||
Documento = null;
|
||||
|
||||
Selecao = new MapaSelecaoOperacao();
|
||||
|
||||
RotaRetorno = new MapaRotaRetorno();
|
||||
}
|
||||
|
||||
public void DefinirEstado(MapaDocumento documento, MapaSelecaoOperacao selecao, MapaRotaRetorno rotaRetorno)
|
||||
{
|
||||
Documento =
|
||||
documento
|
||||
?? throw new ArgumentNullException(
|
||||
nameof(documento)
|
||||
);
|
||||
|
||||
Selecao =
|
||||
selecao
|
||||
?? throw new ArgumentNullException(
|
||||
nameof(selecao)
|
||||
);
|
||||
|
||||
RotaRetorno =
|
||||
rotaRetorno
|
||||
?? throw new ArgumentNullException(
|
||||
nameof(rotaRetorno)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
using AgroBase.Models;
|
||||
using NetTopologySuite.Geometries;
|
||||
using System.Linq;
|
||||
using static AgroBase.Models.Enums;
|
||||
|
||||
namespace OperationControl.Mapas.Models
|
||||
{
|
||||
public class MapaDocumento
|
||||
{
|
||||
public string Nome { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Arquivo que originou este documento.
|
||||
/// Pode deixar de existir futuramente após edições/salvamento,
|
||||
/// portanto não deve ser usado como fonte de verdade do mapa.
|
||||
/// </summary>
|
||||
public string? CaminhoOrigem { get; set; }
|
||||
|
||||
public MapaFormatoOrigem FormatoOrigem { get; set; }
|
||||
= MapaFormatoOrigem.Desconhecido;
|
||||
|
||||
public TipoMapaOperacao Tipo { get; set; }
|
||||
= TipoMapaOperacao.Indefinido;
|
||||
|
||||
/// <summary>
|
||||
/// O documento interno trabalha sempre em WGS84.
|
||||
/// </summary>
|
||||
public int Srid => 4326;
|
||||
|
||||
public List<MapaRua> Ruas { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Indica alteração posterior à importação.
|
||||
/// No carregamento inicial sempre começa false.
|
||||
/// </summary>
|
||||
public bool Modificado { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Avisos que não impediram a importação.
|
||||
///
|
||||
/// Exemplos:
|
||||
/// - SHP sem PRJ, mas já aparentemente em longitude/latitude;
|
||||
/// - PRJ inválido ignorado com segurança;
|
||||
/// - IDs ausentes/duplicados corrigidos.
|
||||
/// </summary>
|
||||
public List<string> AvisosImportacao { get; set; } = new();
|
||||
|
||||
public bool PossuiRuas =>
|
||||
Ruas != null && Ruas.Count > 0;
|
||||
|
||||
public int QuantidadeRuas =>
|
||||
Ruas?.Count ?? 0;
|
||||
|
||||
public double ComprimentoTotalMetros =>
|
||||
Ruas?.Sum(x => x?.ComprimentoMetros ?? 0.0) ?? 0.0;
|
||||
|
||||
/// <summary>
|
||||
/// Limite espacial do documento em WGS84.
|
||||
/// Pode ser usado posteriormente pelo renderizador
|
||||
/// para enquadrar o mapa.
|
||||
/// </summary>
|
||||
public Envelope Limites
|
||||
{
|
||||
get
|
||||
{
|
||||
var envelope = new Envelope();
|
||||
|
||||
if (Ruas == null)
|
||||
return envelope;
|
||||
|
||||
foreach (var rua in Ruas)
|
||||
{
|
||||
if (rua?.Geometria == null ||
|
||||
rua.Geometria.IsEmpty)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
envelope.ExpandToInclude(
|
||||
rua.Geometria.EnvelopeInternal
|
||||
);
|
||||
}
|
||||
|
||||
return envelope;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
using static AgroBase.Models.Enums;
|
||||
|
||||
namespace OperationControl.Mapas.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Estado lógico de uma entidade móvel/fixa exibida no mapa.
|
||||
///
|
||||
/// Não conhece Mapsui.
|
||||
/// Não possui GeometryFeature.
|
||||
/// Não possui Style.
|
||||
/// </summary>
|
||||
public sealed class MapaEntidadeDinamica
|
||||
{
|
||||
public string Id { get; internal set; } = "";
|
||||
|
||||
public MapaTipoEntidadeDinamica Tipo
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
|
||||
|
||||
public string Nome
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
} = "";
|
||||
|
||||
|
||||
public double? Latitude
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
|
||||
public double? Longitude
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
|
||||
|
||||
public double? Heading
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* GNSS bruto, antes da correção de lever arm.
|
||||
*
|
||||
* Mantemos no domínio porque é dado real,
|
||||
* não representação visual.
|
||||
*/
|
||||
public double? LatitudeBruta
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
|
||||
public double? LongitudeBruta
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
|
||||
|
||||
public StatusOperacao Status
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
= StatusOperacao.NaoIniciado;
|
||||
|
||||
|
||||
public bool Pulverizando
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
|
||||
|
||||
public bool EmFoco
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
|
||||
|
||||
public bool PossuiPosicao =>
|
||||
Latitude.HasValue &&
|
||||
Longitude.HasValue;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
namespace OperationControl.Mapas.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Estado vivo das entidades atualmente conhecidas.
|
||||
///
|
||||
/// Independente de mapa carregado e independente da UI.
|
||||
/// </summary>
|
||||
public sealed class MapaEstadoDinamico
|
||||
{
|
||||
private readonly Dictionary<
|
||||
string,
|
||||
MapaEntidadeDinamica>
|
||||
_entidades =
|
||||
new(
|
||||
StringComparer.OrdinalIgnoreCase
|
||||
);
|
||||
|
||||
|
||||
public IReadOnlyCollection<
|
||||
MapaEntidadeDinamica>
|
||||
Entidades =>
|
||||
_entidades.Values;
|
||||
|
||||
|
||||
public int Quantidade =>
|
||||
_entidades.Count;
|
||||
|
||||
|
||||
internal Dictionary<
|
||||
string,
|
||||
MapaEntidadeDinamica>
|
||||
EntidadesMutaveis =>
|
||||
_entidades;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
namespace OperationControl.Mapas.Models
|
||||
{
|
||||
public sealed class MapaEstadoMovimento
|
||||
{
|
||||
private readonly Dictionary<
|
||||
string,
|
||||
MapaMovimentoRover>
|
||||
_rovers =
|
||||
new(
|
||||
StringComparer.OrdinalIgnoreCase
|
||||
);
|
||||
|
||||
|
||||
public IReadOnlyCollection<
|
||||
MapaMovimentoRover>
|
||||
Rovers =>
|
||||
_rovers.Values;
|
||||
|
||||
|
||||
public int Quantidade =>
|
||||
_rovers.Count;
|
||||
|
||||
|
||||
internal Dictionary<
|
||||
string,
|
||||
MapaMovimentoRover>
|
||||
RoversMutaveis =>
|
||||
_rovers;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
namespace OperationControl.Mapas.Models
|
||||
{
|
||||
public sealed class MapaEstimativaCorredor
|
||||
{
|
||||
public string RuaAId { get; set; } = string.Empty;
|
||||
public string RuaBId { get; set; } = string.Empty;
|
||||
|
||||
public double ComprimentoMetros { get; set; }
|
||||
public double LarguraMetros { get; set; }
|
||||
|
||||
public double AreaMetrosQuadrados =>
|
||||
ComprimentoMetros *
|
||||
LarguraMetros;
|
||||
}
|
||||
|
||||
|
||||
public sealed class MapaEstimativaOperacao
|
||||
{
|
||||
public int QuantidadeRuasSelecionadas
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public List<MapaEstimativaCorredor> Corredores
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = new();
|
||||
|
||||
|
||||
public int QuantidadeCorredores =>
|
||||
Corredores?.Count ?? 0;
|
||||
|
||||
|
||||
public double DistanciaMetros
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
|
||||
public double AreaMetrosQuadrados
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
|
||||
public double AreaHectares =>
|
||||
AreaMetrosQuadrados /
|
||||
10000.0;
|
||||
|
||||
|
||||
public double LarguraMediaMetros
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
|
||||
public double? TempoEstimadoSegundos
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
|
||||
public double PercentualErvasConsiderado
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
|
||||
public string? MotivoIndisponivel
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
|
||||
public bool PossuiEstimativa =>
|
||||
QuantidadeCorredores > 0 &&
|
||||
DistanciaMetros > 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
namespace OperationControl.Mapas.Models
|
||||
{
|
||||
public enum MapaFormatoOrigem
|
||||
{
|
||||
Desconhecido = 0,
|
||||
GeoJson = 1,
|
||||
Shapefile = 2,
|
||||
ContratoOperacional = 3
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
namespace OperationControl.Mapas.Models
|
||||
{
|
||||
public sealed class MapaMovimentoRover
|
||||
{
|
||||
private readonly List<MapaTrechoRastro>
|
||||
_trechos = new();
|
||||
|
||||
|
||||
private readonly List<MapaPontoWgs84>
|
||||
_predicao = new();
|
||||
|
||||
|
||||
public string RoverId
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
} = "";
|
||||
|
||||
|
||||
public IReadOnlyList<MapaTrechoRastro>
|
||||
Trechos => _trechos;
|
||||
|
||||
|
||||
public IReadOnlyList<MapaPontoWgs84>
|
||||
Predicao => _predicao;
|
||||
|
||||
|
||||
public MapaPontoWgs84?
|
||||
UltimaPosicaoAceita
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
|
||||
|
||||
public int QuantidadeTrechos =>
|
||||
_trechos.Count;
|
||||
|
||||
|
||||
public int QuantidadePontosPredicao =>
|
||||
_predicao.Count;
|
||||
|
||||
|
||||
public int QuantidadePontosRastro =>
|
||||
_trechos.Sum(
|
||||
x => x.QuantidadePontos
|
||||
);
|
||||
|
||||
|
||||
internal bool QuebrarRastro
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = true;
|
||||
|
||||
|
||||
internal List<MapaTrechoRastro>
|
||||
TrechosMutaveis => _trechos;
|
||||
|
||||
|
||||
internal List<MapaPontoWgs84>
|
||||
PredicaoMutavel => _predicao;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
namespace OperationControl.Mapas.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Ponto WGS84 pertencente à rota manual de retorno.
|
||||
///
|
||||
/// Latitude = eixo norte/sul.
|
||||
/// Longitude = eixo leste/oeste.
|
||||
///
|
||||
/// Não utiliza arrays internamente.
|
||||
/// </summary>
|
||||
public sealed class MapaPontoRetorno
|
||||
{
|
||||
public double Latitude { get; }
|
||||
|
||||
public double Longitude { get; }
|
||||
|
||||
|
||||
public MapaPontoRetorno(
|
||||
double latitude,
|
||||
double longitude)
|
||||
{
|
||||
Latitude = latitude;
|
||||
Longitude = longitude;
|
||||
}
|
||||
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return
|
||||
$"{Latitude:F8}, {Longitude:F8}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
namespace OperationControl.Mapas.Models
|
||||
{
|
||||
public sealed class MapaPontoWgs84
|
||||
{
|
||||
public double Latitude { get; }
|
||||
public double Longitude { get; }
|
||||
|
||||
|
||||
public MapaPontoWgs84(
|
||||
double latitude,
|
||||
double longitude)
|
||||
{
|
||||
Latitude = latitude;
|
||||
Longitude = longitude;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
namespace OperationControl.Mapas.Models
|
||||
{
|
||||
public class MapaResultadoValidacao
|
||||
{
|
||||
public List<string> Erros { get; } = new();
|
||||
|
||||
public List<string> Avisos { get; } = new();
|
||||
|
||||
public bool Valido =>
|
||||
Erros.Count == 0;
|
||||
|
||||
public int QuantidadeErros =>
|
||||
Erros.Count;
|
||||
|
||||
public int QuantidadeAvisos =>
|
||||
Avisos.Count;
|
||||
|
||||
public void AdicionarErro(string mensagem)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(mensagem))
|
||||
Erros.Add(mensagem);
|
||||
}
|
||||
|
||||
public void AdicionarAviso(string mensagem)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(mensagem))
|
||||
Avisos.Add(mensagem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
namespace OperationControl.Mapas.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Representa a rota manual ordenada utilizada
|
||||
/// para retorno seguro do rover à base.
|
||||
///
|
||||
/// A ordem dos pontos é a ordem de passagem.
|
||||
/// </summary>
|
||||
public sealed class MapaRotaRetorno
|
||||
{
|
||||
private readonly List<MapaPontoRetorno>
|
||||
_pontos = new();
|
||||
|
||||
|
||||
public IReadOnlyList<MapaPontoRetorno>
|
||||
Pontos =>
|
||||
_pontos;
|
||||
|
||||
|
||||
public int Quantidade =>
|
||||
_pontos.Count;
|
||||
|
||||
|
||||
public bool PossuiPontos =>
|
||||
_pontos.Count > 0;
|
||||
|
||||
|
||||
internal List<MapaPontoRetorno>
|
||||
PontosMutaveis =>
|
||||
_pontos;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
using NetTopologySuite.Geometries;
|
||||
|
||||
namespace OperationControl.Mapas.Models
|
||||
{
|
||||
public class MapaRua
|
||||
{
|
||||
/// <summary>
|
||||
/// ID canônico utilizado pelo Operation Control e enviado ao rover.
|
||||
/// </summary>
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// ID encontrado no arquivo original, quando existente.
|
||||
/// Pode ser útil para diagnóstico e rastreabilidade.
|
||||
/// </summary>
|
||||
public string? IdOriginal { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indica que o importador precisou criar um ID para esta rua.
|
||||
/// IDs gerados automaticamente são sempre strings numéricas.
|
||||
/// </summary>
|
||||
public bool IdGeradoAutomaticamente { get; set; }
|
||||
|
||||
public string Nome { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Geometria canônica da rua.
|
||||
///
|
||||
/// Regra do módulo:
|
||||
/// todas as geometrias armazenadas no documento estão em
|
||||
/// WGS84 / EPSG:4326:
|
||||
///
|
||||
/// X = Longitude
|
||||
/// Y = Latitude
|
||||
/// </summary>
|
||||
public LineString Geometria { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Comprimento calculado a partir da geometria WGS84.
|
||||
/// </summary>
|
||||
public double ComprimentoMetros { get; set; }
|
||||
|
||||
public double Dist1 { get; set; }
|
||||
|
||||
public double Dist2 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Posição da feature no arquivo original.
|
||||
/// Serve somente para rastreabilidade/diagnóstico.
|
||||
/// </summary>
|
||||
public int IndiceFeatureOrigem { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
namespace OperationControl.Mapas.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Representa a seleção de ruas de uma operação.
|
||||
///
|
||||
/// Não pertence ao mapa físico.
|
||||
/// O mesmo MapaDocumento pode ser utilizado por várias
|
||||
/// operações com seleções e ordens diferentes.
|
||||
/// </summary>
|
||||
public class MapaSelecaoOperacao
|
||||
{
|
||||
private readonly List<string> _ruasOrdenadas = new();
|
||||
|
||||
/// <summary>
|
||||
/// IDs das ruas na ordem em que foram selecionadas
|
||||
/// e deverão ser percorridas.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> RuasOrdenadas =>
|
||||
_ruasOrdenadas;
|
||||
|
||||
public int Quantidade =>
|
||||
_ruasOrdenadas.Count;
|
||||
|
||||
public bool PossuiSelecao =>
|
||||
_ruasOrdenadas.Count > 0;
|
||||
|
||||
|
||||
/*
|
||||
* O serviço de seleção é o único responsável por
|
||||
* modificar esta lista.
|
||||
*/
|
||||
internal List<string> RuasMutaveis =>
|
||||
_ruasOrdenadas;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
namespace OperationControl.Mapas.Models
|
||||
{
|
||||
public enum MapaTipoEntidadeDinamica
|
||||
{
|
||||
Base = 1,
|
||||
Rover = 2
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
namespace OperationControl.Mapas.Models
|
||||
{
|
||||
public sealed class MapaTrechoRastro
|
||||
{
|
||||
private readonly List<MapaPontoWgs84>
|
||||
_pontos = new();
|
||||
|
||||
|
||||
public bool Pulverizando
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
|
||||
|
||||
public IReadOnlyList<MapaPontoWgs84>
|
||||
Pontos => _pontos;
|
||||
|
||||
|
||||
public int QuantidadePontos =>
|
||||
_pontos.Count;
|
||||
|
||||
|
||||
internal List<MapaPontoWgs84>
|
||||
PontosMutaveis => _pontos;
|
||||
|
||||
|
||||
internal MapaTrechoRastro(
|
||||
bool pulverizando)
|
||||
{
|
||||
Pulverizando =
|
||||
pulverizando;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,261 @@
|
|||
using Mapsui;
|
||||
using Mapsui.Layers;
|
||||
using Mapsui.Nts;
|
||||
using Mapsui.Projections;
|
||||
using Mapsui.Styles;
|
||||
using NetTopologySuite.Geometries;
|
||||
using OperationControl.Mapas.Editing;
|
||||
using Brush = Mapsui.Styles.Brush;
|
||||
using Color = Mapsui.Styles.Color;
|
||||
using Font = Mapsui.Styles.Font;
|
||||
using Pen = Mapsui.Styles.Pen;
|
||||
|
||||
namespace OperationControl.Mapas.Rendering
|
||||
{
|
||||
public sealed class MapaBordaGnssRenderService
|
||||
{
|
||||
public const string NomeCamada =
|
||||
"MapaBordaGnssEdicao";
|
||||
|
||||
|
||||
public MemoryLayer CriarCamada(
|
||||
MapaEstadoRecorte estado)
|
||||
{
|
||||
if (estado == null)
|
||||
{
|
||||
throw new ArgumentNullException(
|
||||
nameof(estado)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return new MemoryLayer
|
||||
{
|
||||
Name =
|
||||
NomeCamada,
|
||||
|
||||
Features =
|
||||
CriarFeatures(
|
||||
estado
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public void AtualizarCamada(
|
||||
MemoryLayer camada,
|
||||
MapaEstadoRecorte 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(
|
||||
MapaEstadoRecorte estado)
|
||||
{
|
||||
var features =
|
||||
new List<IFeature>();
|
||||
|
||||
|
||||
if (
|
||||
estado.Tipo !=
|
||||
MapaTipoRecorte.BordaGnss
|
||||
)
|
||||
{
|
||||
return features;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* LINHA
|
||||
*/
|
||||
if (
|
||||
estado.Pontos.Count >= 2
|
||||
)
|
||||
{
|
||||
Coordinate[] coordenadas =
|
||||
estado
|
||||
.Pontos
|
||||
.Select(
|
||||
ponto =>
|
||||
{
|
||||
var mercator =
|
||||
SphericalMercator
|
||||
.FromLonLat(
|
||||
ponto.X,
|
||||
ponto.Y
|
||||
);
|
||||
|
||||
|
||||
return new Coordinate(
|
||||
mercator.x,
|
||||
mercator.y
|
||||
);
|
||||
}
|
||||
)
|
||||
.ToArray();
|
||||
|
||||
|
||||
var featureLinha =
|
||||
new GeometryFeature
|
||||
{
|
||||
Geometry =
|
||||
new LineString(
|
||||
coordenadas
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
featureLinha["Tipo"] =
|
||||
"BordaGnss";
|
||||
|
||||
|
||||
featureLinha.Styles.Add(
|
||||
new VectorStyle
|
||||
{
|
||||
Line =
|
||||
new Pen(
|
||||
new Color(
|
||||
0,
|
||||
210,
|
||||
255,
|
||||
255
|
||||
),
|
||||
4
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
features.Add(
|
||||
featureLinha
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* PONTOS
|
||||
*/
|
||||
for (
|
||||
int i = 0;
|
||||
i < estado.Pontos.Count;
|
||||
i++)
|
||||
{
|
||||
Coordinate ponto =
|
||||
estado.Pontos[i];
|
||||
|
||||
|
||||
var mercator =
|
||||
SphericalMercator
|
||||
.FromLonLat(
|
||||
ponto.X,
|
||||
ponto.Y
|
||||
);
|
||||
|
||||
|
||||
var feature =
|
||||
new PointFeature(
|
||||
mercator.x,
|
||||
mercator.y
|
||||
);
|
||||
|
||||
|
||||
feature["Tipo"] =
|
||||
"PontoBordaGnss";
|
||||
|
||||
feature["Ordem"] =
|
||||
i + 1;
|
||||
|
||||
feature["label"] =
|
||||
(i + 1).ToString();
|
||||
|
||||
|
||||
feature.Styles.Add(
|
||||
new SymbolStyle
|
||||
{
|
||||
SymbolType =
|
||||
SymbolType.Ellipse,
|
||||
|
||||
SymbolScale =
|
||||
1.15,
|
||||
|
||||
Fill =
|
||||
new Brush(
|
||||
new Color(
|
||||
0,
|
||||
210,
|
||||
255,
|
||||
255
|
||||
)
|
||||
),
|
||||
|
||||
Outline =
|
||||
new Pen(
|
||||
Color.White,
|
||||
2
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
feature.Styles.Add(
|
||||
new LabelStyle
|
||||
{
|
||||
LabelColumn =
|
||||
"label",
|
||||
|
||||
ForeColor =
|
||||
Color.White,
|
||||
|
||||
BackColor =
|
||||
null,
|
||||
|
||||
HorizontalAlignment =
|
||||
LabelStyle
|
||||
.HorizontalAlignmentEnum
|
||||
.Center,
|
||||
|
||||
VerticalAlignment =
|
||||
LabelStyle
|
||||
.VerticalAlignmentEnum
|
||||
.Center,
|
||||
|
||||
Font =
|
||||
new Font
|
||||
{
|
||||
Size = 11
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
features.Add(
|
||||
feature
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return features;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,365 @@
|
|||
using Mapsui;
|
||||
using Mapsui.Layers;
|
||||
using Mapsui.Nts;
|
||||
using Mapsui.Projections;
|
||||
using Mapsui.Providers;
|
||||
using Mapsui.Styles;
|
||||
using NetTopologySuite.Geometries;
|
||||
using OperationControl.Mapas.Editing;
|
||||
using Color = Mapsui.Styles.Color;
|
||||
using Pen = Mapsui.Styles.Pen;
|
||||
|
||||
namespace OperationControl.Mapas.Rendering
|
||||
{
|
||||
public sealed class MapaEdicaoRenderService
|
||||
{
|
||||
public const string NomeCamada =
|
||||
"MapaEdicaoNovo";
|
||||
|
||||
|
||||
private static readonly VectorStyle
|
||||
EstiloNormal =
|
||||
new()
|
||||
{
|
||||
Line =
|
||||
new Pen(
|
||||
new Color(
|
||||
46,
|
||||
204,
|
||||
113,
|
||||
210
|
||||
),
|
||||
2
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* Cor própria da edição.
|
||||
*
|
||||
* Não confundir com o amarelo usado
|
||||
* na seleção operacional da missão.
|
||||
*/
|
||||
private static readonly VectorStyle
|
||||
EstiloSelecionado =
|
||||
new()
|
||||
{
|
||||
Line =
|
||||
new Pen(
|
||||
new Color(
|
||||
255,
|
||||
64,
|
||||
190,
|
||||
255
|
||||
),
|
||||
5
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
public MapaRenderResult CriarCamada(
|
||||
MapaEstadoEdicao estado)
|
||||
{
|
||||
if (estado == null)
|
||||
{
|
||||
throw new ArgumentNullException(
|
||||
nameof(estado)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (
|
||||
!estado.EmEdicao ||
|
||||
estado.DocumentoTrabalho == null
|
||||
)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Não existe uma sessão de edição ativa."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
var featuresPorRuaId =
|
||||
new Dictionary<
|
||||
string,
|
||||
GeometryFeature>(
|
||||
StringComparer.OrdinalIgnoreCase
|
||||
);
|
||||
|
||||
|
||||
var features =
|
||||
new List<IFeature>();
|
||||
|
||||
|
||||
foreach (
|
||||
var rua
|
||||
in estado.DocumentoTrabalho.Ruas)
|
||||
{
|
||||
if (
|
||||
rua == null ||
|
||||
rua.Geometria == null ||
|
||||
rua.Geometria.IsEmpty ||
|
||||
rua.Geometria.NumPoints < 2
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Documento canônico:
|
||||
*
|
||||
* EPSG:4326
|
||||
* X = longitude
|
||||
* Y = latitude
|
||||
*
|
||||
* Mapsui:
|
||||
*
|
||||
* desenhamos diretamente em
|
||||
* WebMercator EPSG:3857.
|
||||
*/
|
||||
var coordenadas =
|
||||
rua.Geometria
|
||||
.Coordinates
|
||||
.Select(
|
||||
coordenada =>
|
||||
{
|
||||
var mercator =
|
||||
SphericalMercator
|
||||
.FromLonLat(
|
||||
coordenada.X,
|
||||
coordenada.Y
|
||||
);
|
||||
|
||||
|
||||
return new Coordinate(
|
||||
mercator.x,
|
||||
mercator.y
|
||||
);
|
||||
}
|
||||
)
|
||||
.ToArray();
|
||||
|
||||
|
||||
if (coordenadas.Length < 2)
|
||||
continue;
|
||||
|
||||
|
||||
var feature =
|
||||
new GeometryFeature
|
||||
{
|
||||
Geometry =
|
||||
new LineString(
|
||||
coordenadas
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* Mantemos os mesmos atributos
|
||||
* esperados pelo hit-test atual.
|
||||
*/
|
||||
feature["RuaId"] =
|
||||
rua.Id;
|
||||
|
||||
feature["Id"] =
|
||||
rua.Id;
|
||||
|
||||
feature["Tipo"] =
|
||||
"RuaEdicao";
|
||||
|
||||
|
||||
bool selecionada =
|
||||
EstaSelecionada(
|
||||
estado,
|
||||
rua.Id
|
||||
);
|
||||
|
||||
|
||||
feature
|
||||
.Styles
|
||||
.Add(
|
||||
selecionada
|
||||
? EstiloSelecionado
|
||||
: EstiloNormal
|
||||
);
|
||||
|
||||
|
||||
features.Add(
|
||||
feature
|
||||
);
|
||||
|
||||
|
||||
featuresPorRuaId[
|
||||
rua.Id
|
||||
] =
|
||||
feature;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* MapaRenderResult trabalha com Layer,
|
||||
* portanto seguimos exatamente esse
|
||||
* contrato.
|
||||
*/
|
||||
var provider =
|
||||
new MemoryProvider(
|
||||
features
|
||||
);
|
||||
|
||||
|
||||
var camada =
|
||||
new Layer
|
||||
{
|
||||
Name =
|
||||
NomeCamada,
|
||||
|
||||
DataSource =
|
||||
provider,
|
||||
|
||||
/*
|
||||
* Cada feature possui seu
|
||||
* próprio estilo.
|
||||
*/
|
||||
Style =
|
||||
null
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* IMPORTANTE:
|
||||
*
|
||||
* MapaRenderResult NÃO possui
|
||||
* construtor vazio.
|
||||
*
|
||||
* Layer e dicionário entram pelo
|
||||
* construtor obrigatório.
|
||||
*/
|
||||
return new MapaRenderResult(
|
||||
camada,
|
||||
featuresPorRuaId
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public void AtualizarEstiloRua(
|
||||
MapaRenderResult render,
|
||||
MapaEstadoEdicao estado,
|
||||
string ruaId)
|
||||
{
|
||||
if (
|
||||
render == null ||
|
||||
estado == null ||
|
||||
string.IsNullOrWhiteSpace(
|
||||
ruaId
|
||||
)
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Aproveitamos a API que já existe
|
||||
* no próprio MapaRenderResult.
|
||||
*/
|
||||
var feature =
|
||||
render.ObterFeature(
|
||||
ruaId
|
||||
);
|
||||
|
||||
|
||||
if (feature == null)
|
||||
return;
|
||||
|
||||
|
||||
bool selecionada =
|
||||
EstaSelecionada(
|
||||
estado,
|
||||
ruaId
|
||||
);
|
||||
|
||||
|
||||
feature
|
||||
.Styles
|
||||
.Clear();
|
||||
|
||||
|
||||
feature
|
||||
.Styles
|
||||
.Add(
|
||||
selecionada
|
||||
? EstiloSelecionado
|
||||
: EstiloNormal
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public void AplicarSelecao(
|
||||
MapaRenderResult render,
|
||||
MapaEstadoEdicao estado)
|
||||
{
|
||||
if (
|
||||
render == null ||
|
||||
estado == null
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
foreach (
|
||||
var item
|
||||
in render.FeaturesPorRuaId)
|
||||
{
|
||||
bool selecionada =
|
||||
EstaSelecionada(
|
||||
estado,
|
||||
item.Key
|
||||
);
|
||||
|
||||
|
||||
item.Value
|
||||
.Styles
|
||||
.Clear();
|
||||
|
||||
|
||||
item.Value
|
||||
.Styles
|
||||
.Add(
|
||||
selecionada
|
||||
? EstiloSelecionado
|
||||
: EstiloNormal
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static bool EstaSelecionada(
|
||||
MapaEstadoEdicao estado,
|
||||
string ruaId)
|
||||
{
|
||||
if (
|
||||
string.IsNullOrWhiteSpace(
|
||||
ruaId
|
||||
)
|
||||
)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
return estado
|
||||
.RuasSelecionadas
|
||||
.Any(
|
||||
id =>
|
||||
string.Equals(
|
||||
id,
|
||||
ruaId,
|
||||
StringComparison
|
||||
.OrdinalIgnoreCase
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,606 @@
|
|||
using Mapsui;
|
||||
using Mapsui.Layers;
|
||||
using Mapsui.Nts;
|
||||
using Mapsui.Styles;
|
||||
using NetTopologySuite.Geometries;
|
||||
using OperationControl.Mapas.Models;
|
||||
using Brush = Mapsui.Styles.Brush;
|
||||
using Font = Mapsui.Styles.Font;
|
||||
using Pen = Mapsui.Styles.Pen;
|
||||
|
||||
namespace OperationControl.Mapas.Rendering
|
||||
{
|
||||
/// <summary>
|
||||
/// Converte o estado dinâmico de Base/Rovers
|
||||
/// em representação visual Mapsui.
|
||||
///
|
||||
/// Não armazena estado operacional.
|
||||
/// </summary>
|
||||
public sealed class MapaEntidadeDinamicaRenderService
|
||||
{
|
||||
public const string NomeCamada =
|
||||
"MapaEntidadesDinamicasNovo";
|
||||
|
||||
|
||||
public MemoryLayer CriarCamada(
|
||||
MapaEstadoDinamico estado,
|
||||
double resolution)
|
||||
{
|
||||
if (estado == null)
|
||||
{
|
||||
throw new ArgumentNullException(
|
||||
nameof(estado)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return new MemoryLayer
|
||||
{
|
||||
Name = NomeCamada,
|
||||
|
||||
Features =
|
||||
CriarFeatures(
|
||||
estado,
|
||||
resolution
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public void AtualizarCamada(
|
||||
MemoryLayer camada,
|
||||
MapaEstadoDinamico estado,
|
||||
double resolution)
|
||||
{
|
||||
if (camada == null)
|
||||
{
|
||||
throw new ArgumentNullException(
|
||||
nameof(camada)
|
||||
);
|
||||
}
|
||||
|
||||
if (estado == null)
|
||||
{
|
||||
throw new ArgumentNullException(
|
||||
nameof(estado)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
camada.Features =
|
||||
CriarFeatures(
|
||||
estado,
|
||||
resolution
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private static IEnumerable<IFeature>
|
||||
CriarFeatures(
|
||||
MapaEstadoDinamico estado,
|
||||
double resolution)
|
||||
{
|
||||
var features =
|
||||
new List<IFeature>();
|
||||
|
||||
|
||||
if (
|
||||
!double.IsFinite(resolution) ||
|
||||
resolution <= 0
|
||||
)
|
||||
{
|
||||
resolution =
|
||||
1.0;
|
||||
}
|
||||
|
||||
|
||||
foreach (
|
||||
var entidade
|
||||
in estado.Entidades)
|
||||
{
|
||||
if (!entidade.PossuiPosicao)
|
||||
continue;
|
||||
|
||||
|
||||
double latitude =
|
||||
entidade.Latitude!.Value;
|
||||
|
||||
double longitude =
|
||||
entidade.Longitude!.Value;
|
||||
|
||||
|
||||
var mercator =
|
||||
Mapsui.Projections
|
||||
.SphericalMercator
|
||||
.FromLonLat(
|
||||
longitude,
|
||||
latitude
|
||||
);
|
||||
|
||||
|
||||
var centro =
|
||||
new MPoint(
|
||||
mercator.x,
|
||||
mercator.y
|
||||
);
|
||||
|
||||
|
||||
if (
|
||||
entidade.Tipo ==
|
||||
MapaTipoEntidadeDinamica.Base
|
||||
)
|
||||
{
|
||||
CriarBase(
|
||||
entidade,
|
||||
centro,
|
||||
resolution,
|
||||
features
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
CriarRover(
|
||||
entidade,
|
||||
centro,
|
||||
resolution,
|
||||
features
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Label e ponto central para futura
|
||||
* identificação/hit-test.
|
||||
*/
|
||||
CriarCentroELabel(
|
||||
entidade,
|
||||
centro,
|
||||
features
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return features;
|
||||
}
|
||||
|
||||
|
||||
private static void CriarRover(
|
||||
MapaEntidadeDinamica entidade,
|
||||
MPoint centro,
|
||||
double resolution,
|
||||
List<IFeature> features)
|
||||
{
|
||||
/*
|
||||
* Aproximadamente 24 px no zoom atual.
|
||||
*/
|
||||
double comprimento =
|
||||
24.0 *
|
||||
resolution;
|
||||
|
||||
double largura =
|
||||
comprimento *
|
||||
0.75;
|
||||
|
||||
|
||||
double frente =
|
||||
comprimento *
|
||||
0.55;
|
||||
|
||||
double traseira =
|
||||
comprimento *
|
||||
0.45;
|
||||
|
||||
double meiaLargura =
|
||||
largura *
|
||||
0.5;
|
||||
|
||||
|
||||
/*
|
||||
* Convenção:
|
||||
*
|
||||
* 0° = Norte
|
||||
* 90° = Leste
|
||||
* 180° = Sul
|
||||
* 270° = Oeste
|
||||
*/
|
||||
double heading =
|
||||
entidade.Heading
|
||||
?? 0.0;
|
||||
|
||||
double rad =
|
||||
heading *
|
||||
Math.PI /
|
||||
180.0;
|
||||
|
||||
|
||||
double ux =
|
||||
Math.Sin(rad);
|
||||
|
||||
double uy =
|
||||
Math.Cos(rad);
|
||||
|
||||
|
||||
double px =
|
||||
Math.Cos(rad);
|
||||
|
||||
double py =
|
||||
-Math.Sin(rad);
|
||||
|
||||
|
||||
var front =
|
||||
new Coordinate(
|
||||
centro.X +
|
||||
ux * frente,
|
||||
|
||||
centro.Y +
|
||||
uy * frente
|
||||
);
|
||||
|
||||
|
||||
var left =
|
||||
new Coordinate(
|
||||
centro.X -
|
||||
ux * traseira +
|
||||
px * meiaLargura,
|
||||
|
||||
centro.Y -
|
||||
uy * traseira +
|
||||
py * meiaLargura
|
||||
);
|
||||
|
||||
|
||||
var right =
|
||||
new Coordinate(
|
||||
centro.X -
|
||||
ux * traseira -
|
||||
px * meiaLargura,
|
||||
|
||||
centro.Y -
|
||||
uy * traseira -
|
||||
py * meiaLargura
|
||||
);
|
||||
|
||||
|
||||
var ring =
|
||||
new LinearRing(
|
||||
new[]
|
||||
{
|
||||
front,
|
||||
left,
|
||||
right,
|
||||
front
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
var feature =
|
||||
new GeometryFeature
|
||||
{
|
||||
Geometry =
|
||||
new Polygon(
|
||||
ring
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
feature["EntidadeId"] =
|
||||
entidade.Id;
|
||||
|
||||
feature["Tipo"] =
|
||||
"Rover";
|
||||
|
||||
|
||||
var cor =
|
||||
new Mapsui.Styles.Color(
|
||||
45,
|
||||
140,
|
||||
220,
|
||||
255
|
||||
);
|
||||
|
||||
|
||||
feature.Styles.Add(
|
||||
new VectorStyle
|
||||
{
|
||||
Fill =
|
||||
new Brush(
|
||||
cor
|
||||
),
|
||||
|
||||
Outline =
|
||||
new Pen(
|
||||
Mapsui.Styles.Color.White,
|
||||
entidade.EmFoco
|
||||
? 3
|
||||
: 2
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
features.Add(
|
||||
feature
|
||||
);
|
||||
|
||||
|
||||
/*
|
||||
* Halo de foco.
|
||||
*/
|
||||
if (entidade.EmFoco)
|
||||
{
|
||||
var foco =
|
||||
new PointFeature(
|
||||
centro
|
||||
);
|
||||
|
||||
|
||||
foco["EntidadeId"] =
|
||||
entidade.Id;
|
||||
|
||||
|
||||
foco.Styles.Add(
|
||||
new SymbolStyle
|
||||
{
|
||||
SymbolType =
|
||||
SymbolType.Ellipse,
|
||||
|
||||
SymbolScale =
|
||||
1.35,
|
||||
|
||||
Fill =
|
||||
new Brush(
|
||||
new Mapsui.Styles.Color(
|
||||
cor.R,
|
||||
cor.G,
|
||||
cor.B,
|
||||
40
|
||||
)
|
||||
),
|
||||
|
||||
Outline =
|
||||
new Pen(
|
||||
new Mapsui.Styles.Color(
|
||||
cor.R,
|
||||
cor.G,
|
||||
cor.B,
|
||||
180
|
||||
),
|
||||
2
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
features.Add(
|
||||
foco
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static void CriarBase(
|
||||
MapaEntidadeDinamica entidade,
|
||||
MPoint centro,
|
||||
double resolution,
|
||||
List<IFeature> features)
|
||||
{
|
||||
double raio =
|
||||
22.0 *
|
||||
resolution;
|
||||
|
||||
|
||||
var coords =
|
||||
new List<Coordinate>();
|
||||
|
||||
|
||||
/*
|
||||
* Hexágono.
|
||||
*/
|
||||
for (
|
||||
int i = 0;
|
||||
i < 6;
|
||||
i++)
|
||||
{
|
||||
double ang =
|
||||
Math.PI / 6.0 +
|
||||
i *
|
||||
Math.PI / 3.0;
|
||||
|
||||
|
||||
coords.Add(
|
||||
new Coordinate(
|
||||
centro.X +
|
||||
Math.Cos(ang) *
|
||||
raio,
|
||||
|
||||
centro.Y +
|
||||
Math.Sin(ang) *
|
||||
raio
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
coords.Add(
|
||||
coords[0]
|
||||
);
|
||||
|
||||
|
||||
var feature =
|
||||
new GeometryFeature
|
||||
{
|
||||
Geometry =
|
||||
new Polygon(
|
||||
new LinearRing(
|
||||
coords.ToArray()
|
||||
)
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
feature["EntidadeId"] =
|
||||
entidade.Id;
|
||||
|
||||
feature["Tipo"] =
|
||||
"Base";
|
||||
|
||||
|
||||
var cor =
|
||||
new Mapsui.Styles.Color(
|
||||
220,
|
||||
45,
|
||||
45,
|
||||
255
|
||||
);
|
||||
|
||||
|
||||
feature.Styles.Add(
|
||||
new VectorStyle
|
||||
{
|
||||
Fill =
|
||||
new Brush(
|
||||
cor
|
||||
),
|
||||
|
||||
Outline =
|
||||
new Mapsui.Styles.Pen(
|
||||
Mapsui.Styles.Color.White,
|
||||
entidade.EmFoco
|
||||
? 3
|
||||
: 2
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
features.Add(
|
||||
feature
|
||||
);
|
||||
|
||||
|
||||
/*
|
||||
* Centro branco da base.
|
||||
*/
|
||||
var centroFeature =
|
||||
new PointFeature(
|
||||
centro
|
||||
);
|
||||
|
||||
|
||||
centroFeature["EntidadeId"] =
|
||||
entidade.Id;
|
||||
|
||||
|
||||
centroFeature.Styles.Add(
|
||||
new SymbolStyle
|
||||
{
|
||||
SymbolType =
|
||||
SymbolType.Ellipse,
|
||||
|
||||
SymbolScale =
|
||||
0.45,
|
||||
|
||||
Fill =
|
||||
new Brush(
|
||||
Mapsui.Styles.Color.White
|
||||
),
|
||||
|
||||
Outline =
|
||||
null
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
features.Add(
|
||||
centroFeature
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private static void CriarCentroELabel(
|
||||
MapaEntidadeDinamica entidade,
|
||||
MPoint centro,
|
||||
List<IFeature> features)
|
||||
{
|
||||
var ponto =
|
||||
new PointFeature(
|
||||
centro
|
||||
);
|
||||
|
||||
|
||||
/*
|
||||
* Isso será muito útil no 7C:
|
||||
*
|
||||
* feature visual -> EntidadeId
|
||||
*/
|
||||
ponto["EntidadeId"] =
|
||||
entidade.Id;
|
||||
|
||||
ponto["Tipo"] =
|
||||
entidade.Tipo.ToString();
|
||||
|
||||
|
||||
/*
|
||||
* Pequeno ponto central.
|
||||
*/
|
||||
ponto.Styles.Add(
|
||||
new SymbolStyle
|
||||
{
|
||||
SymbolScale =
|
||||
0.10,
|
||||
|
||||
Fill =
|
||||
new Brush(
|
||||
Mapsui.Styles.Color.White
|
||||
),
|
||||
|
||||
Outline =
|
||||
null
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
ponto.Styles.Add(
|
||||
new LabelStyle
|
||||
{
|
||||
Text =
|
||||
entidade.Nome,
|
||||
|
||||
ForeColor =
|
||||
Mapsui.Styles.Color.White,
|
||||
|
||||
BackColor =
|
||||
new Brush(
|
||||
new Mapsui.Styles.Color(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
150
|
||||
)
|
||||
),
|
||||
|
||||
Offset =
|
||||
new Offset(
|
||||
0,
|
||||
-34
|
||||
),
|
||||
|
||||
Font =
|
||||
new Font
|
||||
{
|
||||
FontFamily =
|
||||
"Segoe UI",
|
||||
|
||||
Size =
|
||||
15
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
features.Add(
|
||||
ponto
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
using Mapsui;
|
||||
using Mapsui.Layers;
|
||||
using Mapsui.Nts;
|
||||
using Mapsui.Projections;
|
||||
using Mapsui.Styles;
|
||||
using NetTopologySuite.Geometries;
|
||||
using OperationControl.Mapas.Models;
|
||||
using Color = Mapsui.Styles.Color;
|
||||
using Pen = Mapsui.Styles.Pen;
|
||||
|
||||
namespace OperationControl.Mapas.Rendering
|
||||
{
|
||||
public sealed class MapaPredicaoMpcRenderService
|
||||
{
|
||||
public const string NomeCamada =
|
||||
"MapaPredicaoMpcNova";
|
||||
|
||||
|
||||
public MemoryLayer CriarCamada(
|
||||
MapaEstadoMovimento estado)
|
||||
{
|
||||
if (estado == null)
|
||||
{
|
||||
throw new ArgumentNullException(
|
||||
nameof(estado)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return new MemoryLayer
|
||||
{
|
||||
Name = NomeCamada,
|
||||
Features =
|
||||
CriarFeatures(
|
||||
estado
|
||||
),
|
||||
Style = null
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public void AtualizarCamada(
|
||||
MemoryLayer camada,
|
||||
MapaEstadoMovimento estado)
|
||||
{
|
||||
if (camada == null)
|
||||
{
|
||||
throw new ArgumentNullException(
|
||||
nameof(camada)
|
||||
);
|
||||
}
|
||||
|
||||
if (estado == null)
|
||||
{
|
||||
throw new ArgumentNullException(
|
||||
nameof(estado)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
camada.Features =
|
||||
CriarFeatures(
|
||||
estado
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private static List<IFeature>
|
||||
CriarFeatures(
|
||||
MapaEstadoMovimento estado)
|
||||
{
|
||||
var features =
|
||||
new List<IFeature>();
|
||||
|
||||
|
||||
foreach (
|
||||
var movimento
|
||||
in estado.Rovers)
|
||||
{
|
||||
if (
|
||||
movimento.Predicao == null ||
|
||||
movimento.Predicao.Count < 2
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
var coordenadas =
|
||||
new List<Coordinate>();
|
||||
|
||||
|
||||
foreach (
|
||||
var ponto
|
||||
in movimento.Predicao)
|
||||
{
|
||||
var mercator =
|
||||
SphericalMercator
|
||||
.FromLonLat(
|
||||
ponto.Longitude,
|
||||
ponto.Latitude
|
||||
);
|
||||
|
||||
|
||||
coordenadas.Add(
|
||||
new Coordinate(
|
||||
mercator.x,
|
||||
mercator.y
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (
|
||||
coordenadas.Count < 2
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
var feature =
|
||||
new GeometryFeature
|
||||
{
|
||||
Geometry =
|
||||
new LineString(
|
||||
coordenadas
|
||||
.ToArray()
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
feature["RoverId"] =
|
||||
movimento.RoverId;
|
||||
|
||||
feature["Tipo"] =
|
||||
"Predict";
|
||||
|
||||
|
||||
feature.Styles.Add(
|
||||
CriarEstilo()
|
||||
);
|
||||
|
||||
|
||||
features.Add(
|
||||
feature
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return features;
|
||||
}
|
||||
|
||||
|
||||
private static VectorStyle
|
||||
CriarEstilo()
|
||||
{
|
||||
return new VectorStyle
|
||||
{
|
||||
Line =
|
||||
new Pen
|
||||
{
|
||||
Color =
|
||||
Color.Lime,
|
||||
|
||||
Width =
|
||||
2,
|
||||
|
||||
PenStyle =
|
||||
PenStyle.Dash
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,287 @@
|
|||
using Mapsui;
|
||||
using Mapsui.Layers;
|
||||
using Mapsui.Nts;
|
||||
using Mapsui.Projections;
|
||||
using Mapsui.Styles;
|
||||
using NetTopologySuite.Geometries;
|
||||
using OperationControl.Mapas.Models;
|
||||
using Color = Mapsui.Styles.Color;
|
||||
using Pen = Mapsui.Styles.Pen;
|
||||
|
||||
namespace OperationControl.Mapas.Rendering
|
||||
{
|
||||
public sealed class MapaRastroRenderService
|
||||
{
|
||||
public const string NomeCamada =
|
||||
"MapaRastroNovo";
|
||||
|
||||
|
||||
/*
|
||||
* Mesma ideia da paleta do legado:
|
||||
* cada RoverId recebe uma cor estável.
|
||||
*
|
||||
* A cor é responsabilidade de render,
|
||||
* portanto não precisa entrar no domínio.
|
||||
*/
|
||||
private static readonly Color[] Paleta =
|
||||
{
|
||||
new Color(0, 191, 255, 255), // DeepSkyBlue
|
||||
new Color(50, 205, 50, 255), // LimeGreen
|
||||
new Color(255, 215, 0, 255), // Gold
|
||||
new Color(255, 165, 0, 255), // Orange
|
||||
new Color(147, 112, 219, 255), // MediumPurple
|
||||
new Color(0, 255, 255, 255), // Cyan
|
||||
new Color(255, 0, 255, 255), // Magenta
|
||||
new Color(0, 255, 127, 255), // SpringGreen
|
||||
new Color(255, 127, 80, 255), // Coral
|
||||
new Color(255, 255, 0, 255) // Yellow
|
||||
};
|
||||
|
||||
|
||||
public MemoryLayer CriarCamada(
|
||||
MapaEstadoMovimento estado)
|
||||
{
|
||||
if (estado == null)
|
||||
{
|
||||
throw new ArgumentNullException(
|
||||
nameof(estado)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return new MemoryLayer
|
||||
{
|
||||
Name = NomeCamada,
|
||||
Features = CriarFeatures(
|
||||
estado
|
||||
),
|
||||
Style = null
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public void AtualizarCamada(
|
||||
MemoryLayer camada,
|
||||
MapaEstadoMovimento estado)
|
||||
{
|
||||
if (camada == null)
|
||||
{
|
||||
throw new ArgumentNullException(
|
||||
nameof(camada)
|
||||
);
|
||||
}
|
||||
|
||||
if (estado == null)
|
||||
{
|
||||
throw new ArgumentNullException(
|
||||
nameof(estado)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
camada.Features =
|
||||
CriarFeatures(
|
||||
estado
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private static List<IFeature>
|
||||
CriarFeatures(
|
||||
MapaEstadoMovimento estado)
|
||||
{
|
||||
var features =
|
||||
new List<IFeature>();
|
||||
|
||||
|
||||
foreach (
|
||||
var movimento
|
||||
in estado.Rovers)
|
||||
{
|
||||
Color corRover =
|
||||
ObterCorRover(
|
||||
movimento.RoverId
|
||||
);
|
||||
|
||||
|
||||
foreach (
|
||||
var trecho
|
||||
in movimento.Trechos)
|
||||
{
|
||||
if (
|
||||
trecho == null ||
|
||||
trecho.Pontos.Count < 2
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
var coordenadas =
|
||||
new List<Coordinate>();
|
||||
|
||||
|
||||
foreach (
|
||||
var ponto
|
||||
in trecho.Pontos)
|
||||
{
|
||||
var mercator =
|
||||
SphericalMercator
|
||||
.FromLonLat(
|
||||
ponto.Longitude,
|
||||
ponto.Latitude
|
||||
);
|
||||
|
||||
|
||||
coordenadas.Add(
|
||||
new Coordinate(
|
||||
mercator.x,
|
||||
mercator.y
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (
|
||||
coordenadas.Count < 2
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
var feature =
|
||||
new GeometryFeature
|
||||
{
|
||||
Geometry =
|
||||
new LineString(
|
||||
coordenadas
|
||||
.ToArray()
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
feature["RoverId"] =
|
||||
movimento.RoverId;
|
||||
|
||||
feature["Tipo"] =
|
||||
"Rastro";
|
||||
|
||||
feature["Pulverizando"] =
|
||||
trecho.Pulverizando
|
||||
? "1"
|
||||
: "0";
|
||||
|
||||
|
||||
feature.Styles.Add(
|
||||
CriarEstilo(
|
||||
corRover,
|
||||
trecho.Pulverizando
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
features.Add(
|
||||
feature
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return features;
|
||||
}
|
||||
|
||||
|
||||
private static VectorStyle CriarEstilo(
|
||||
Color cor,
|
||||
bool pulverizando)
|
||||
{
|
||||
/*
|
||||
* Mantemos a semântica visual do legado:
|
||||
*
|
||||
* normal:
|
||||
* mais transparente e fino
|
||||
*
|
||||
* pulverizando:
|
||||
* opaco e mais grosso
|
||||
*/
|
||||
Color corLinha =
|
||||
pulverizando
|
||||
? new Color(
|
||||
cor.R,
|
||||
cor.G,
|
||||
cor.B,
|
||||
255
|
||||
)
|
||||
: new Color(
|
||||
cor.R,
|
||||
cor.G,
|
||||
cor.B,
|
||||
180
|
||||
);
|
||||
|
||||
|
||||
return new VectorStyle
|
||||
{
|
||||
Line =
|
||||
new Pen(
|
||||
corLinha,
|
||||
pulverizando
|
||||
? 4
|
||||
: 2
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private static Color ObterCorRover(
|
||||
string roverId)
|
||||
{
|
||||
if (
|
||||
string.IsNullOrWhiteSpace(
|
||||
roverId
|
||||
)
|
||||
)
|
||||
{
|
||||
return Paleta[0];
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Hash determinístico.
|
||||
*
|
||||
* Não usamos string.GetHashCode()
|
||||
* porque ele não precisa ser estável
|
||||
* entre execuções do processo.
|
||||
*/
|
||||
uint hash =
|
||||
2166136261;
|
||||
|
||||
|
||||
foreach (
|
||||
char caractere
|
||||
in roverId
|
||||
.Trim()
|
||||
.ToUpperInvariant())
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
hash ^= caractere;
|
||||
hash *= 16777619;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
int indice =
|
||||
(int)(
|
||||
hash %
|
||||
(uint)Paleta.Length
|
||||
);
|
||||
|
||||
|
||||
return Paleta[
|
||||
indice
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,402 @@
|
|||
using Mapsui;
|
||||
using Mapsui.Layers;
|
||||
using Mapsui.Nts;
|
||||
using Mapsui.Projections;
|
||||
using Mapsui.Styles;
|
||||
using NetTopologySuite.Geometries;
|
||||
using OperationControl.Mapas.Models;
|
||||
using Brush = Mapsui.Styles.Brush;
|
||||
using Color = Mapsui.Styles.Color;
|
||||
using Pen = Mapsui.Styles.Pen;
|
||||
|
||||
namespace OperationControl.Mapas.Rendering
|
||||
{
|
||||
public sealed class MapaReferenciaGnssRenderService
|
||||
{
|
||||
public const string NomeCamada =
|
||||
"MapaReferenciasGnssNovo";
|
||||
|
||||
|
||||
public MemoryLayer CriarCamada(
|
||||
MapaEstadoDinamico estado)
|
||||
{
|
||||
if (estado == null)
|
||||
{
|
||||
throw new ArgumentNullException(
|
||||
nameof(estado)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return new MemoryLayer
|
||||
{
|
||||
Name = NomeCamada,
|
||||
Features =
|
||||
CriarFeatures(
|
||||
estado
|
||||
),
|
||||
Style = null
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public void AtualizarCamada(
|
||||
MemoryLayer camada,
|
||||
MapaEstadoDinamico estado)
|
||||
{
|
||||
if (camada == null)
|
||||
{
|
||||
throw new ArgumentNullException(
|
||||
nameof(camada)
|
||||
);
|
||||
}
|
||||
|
||||
if (estado == null)
|
||||
{
|
||||
throw new ArgumentNullException(
|
||||
nameof(estado)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
camada.Features =
|
||||
CriarFeatures(
|
||||
estado
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private static List<IFeature>
|
||||
CriarFeatures(
|
||||
MapaEstadoDinamico estado)
|
||||
{
|
||||
var features =
|
||||
new List<IFeature>();
|
||||
|
||||
|
||||
foreach (
|
||||
var entidade
|
||||
in estado.Entidades)
|
||||
{
|
||||
if (
|
||||
entidade == null ||
|
||||
!CoordenadaValida(
|
||||
entidade.Latitude,
|
||||
entidade.Longitude
|
||||
) ||
|
||||
!CoordenadaValida(
|
||||
entidade.LatitudeBruta,
|
||||
entidade.LongitudeBruta
|
||||
)
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Posição utilizada pelo sistema,
|
||||
* já considerando a correção aplicada.
|
||||
*/
|
||||
var corrigido =
|
||||
SphericalMercator
|
||||
.FromLonLat(
|
||||
entidade.Longitude!.Value,
|
||||
entidade.Latitude!.Value
|
||||
);
|
||||
|
||||
|
||||
/*
|
||||
* Posição GNSS de referência/bruta
|
||||
* anterior à correção de lever arm.
|
||||
*/
|
||||
var bruto =
|
||||
SphericalMercator
|
||||
.FromLonLat(
|
||||
entidade.LongitudeBruta!.Value,
|
||||
entidade.LatitudeBruta!.Value
|
||||
);
|
||||
|
||||
|
||||
CriarLinha(
|
||||
features,
|
||||
entidade,
|
||||
bruto.x,
|
||||
bruto.y,
|
||||
corrigido.x,
|
||||
corrigido.y
|
||||
);
|
||||
|
||||
|
||||
CriarPontoBruto(
|
||||
features,
|
||||
entidade,
|
||||
bruto.x,
|
||||
bruto.y
|
||||
);
|
||||
|
||||
|
||||
CriarPontoCorrigido(
|
||||
features,
|
||||
entidade,
|
||||
corrigido.x,
|
||||
corrigido.y
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return features;
|
||||
}
|
||||
|
||||
|
||||
private static void CriarLinha(
|
||||
List<IFeature> features,
|
||||
MapaEntidadeDinamica entidade,
|
||||
double rawX,
|
||||
double rawY,
|
||||
double corrigidoX,
|
||||
double corrigidoY)
|
||||
{
|
||||
var feature =
|
||||
new GeometryFeature
|
||||
{
|
||||
Geometry =
|
||||
new LineString(
|
||||
new[]
|
||||
{
|
||||
new Coordinate(
|
||||
rawX,
|
||||
rawY
|
||||
),
|
||||
|
||||
new Coordinate(
|
||||
corrigidoX,
|
||||
corrigidoY
|
||||
)
|
||||
}
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
AplicarMetadados(
|
||||
feature,
|
||||
entidade,
|
||||
"LigacaoLeverArm"
|
||||
);
|
||||
|
||||
|
||||
feature.Styles.Add(
|
||||
new VectorStyle
|
||||
{
|
||||
Line =
|
||||
new Pen
|
||||
{
|
||||
Color =
|
||||
new Color(
|
||||
255,
|
||||
255,
|
||||
255,
|
||||
170
|
||||
),
|
||||
|
||||
Width =
|
||||
1,
|
||||
|
||||
PenStyle =
|
||||
PenStyle.Dash
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
features.Add(
|
||||
feature
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private static void CriarPontoBruto(
|
||||
List<IFeature> features,
|
||||
MapaEntidadeDinamica entidade,
|
||||
double x,
|
||||
double y)
|
||||
{
|
||||
var feature =
|
||||
new PointFeature(
|
||||
new MPoint(
|
||||
x,
|
||||
y
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
AplicarMetadados(
|
||||
feature,
|
||||
entidade,
|
||||
"GnssBruto"
|
||||
);
|
||||
|
||||
|
||||
feature.Styles.Add(
|
||||
new SymbolStyle
|
||||
{
|
||||
SymbolType =
|
||||
SymbolType.Ellipse,
|
||||
|
||||
SymbolScale =
|
||||
0.55,
|
||||
|
||||
Fill =
|
||||
new Brush(
|
||||
new Color(
|
||||
255,
|
||||
215,
|
||||
0,
|
||||
255
|
||||
)
|
||||
),
|
||||
|
||||
Outline =
|
||||
new Pen(
|
||||
new Color(
|
||||
20,
|
||||
20,
|
||||
20,
|
||||
230
|
||||
),
|
||||
1
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
features.Add(
|
||||
feature
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private static void CriarPontoCorrigido(
|
||||
List<IFeature> features,
|
||||
MapaEntidadeDinamica entidade,
|
||||
double x,
|
||||
double y)
|
||||
{
|
||||
var feature =
|
||||
new PointFeature(
|
||||
new MPoint(
|
||||
x,
|
||||
y
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
AplicarMetadados(
|
||||
feature,
|
||||
entidade,
|
||||
"GnssCorrigidoLeverArm"
|
||||
);
|
||||
|
||||
|
||||
feature.Styles.Add(
|
||||
new SymbolStyle
|
||||
{
|
||||
SymbolType =
|
||||
SymbolType.Ellipse,
|
||||
|
||||
SymbolScale =
|
||||
0.50,
|
||||
|
||||
Fill =
|
||||
new Brush(
|
||||
Color.White
|
||||
),
|
||||
|
||||
Outline =
|
||||
new Pen(
|
||||
new Color(
|
||||
20,
|
||||
20,
|
||||
20,
|
||||
230
|
||||
),
|
||||
1
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
features.Add(
|
||||
feature
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private static void AplicarMetadados(
|
||||
IFeature feature,
|
||||
MapaEntidadeDinamica entidade,
|
||||
string referencia)
|
||||
{
|
||||
feature["EntidadeId"] =
|
||||
entidade.Id;
|
||||
|
||||
feature["Tipo"] =
|
||||
"ReferenciaGnss";
|
||||
|
||||
feature["Referencia"] =
|
||||
referencia;
|
||||
}
|
||||
|
||||
|
||||
private static bool CoordenadaValida(
|
||||
double? latitude,
|
||||
double? longitude)
|
||||
{
|
||||
if (
|
||||
!latitude.HasValue ||
|
||||
!longitude.HasValue
|
||||
)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (
|
||||
!double.IsFinite(latitude.Value) ||
|
||||
!double.IsFinite(longitude.Value)
|
||||
)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (
|
||||
latitude.Value < -90.0 ||
|
||||
latitude.Value > 90.0 ||
|
||||
longitude.Value < -180.0 ||
|
||||
longitude.Value > 180.0
|
||||
)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Mesma proteção que existia no legado:
|
||||
* não desenhar referência fake em 0,0.
|
||||
*/
|
||||
if (
|
||||
Math.Abs(latitude.Value) <
|
||||
0.0000001 &&
|
||||
Math.Abs(longitude.Value) <
|
||||
0.0000001
|
||||
)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
using Mapsui.Layers;
|
||||
using Mapsui.Nts;
|
||||
|
||||
namespace OperationControl.Mapas.Rendering
|
||||
{
|
||||
/// <summary>
|
||||
/// Resultado da construção visual de um MapaDocumento.
|
||||
///
|
||||
/// Não contém a fonte de verdade do mapa.
|
||||
/// As GeometryFeatures existem apenas como representação
|
||||
/// para o Mapsui.
|
||||
/// </summary>
|
||||
public sealed class MapaRenderResult
|
||||
{
|
||||
public Layer Layer { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, GeometryFeature>
|
||||
FeaturesPorRuaId
|
||||
{ get; }
|
||||
|
||||
|
||||
public int QuantidadeFeatures =>
|
||||
FeaturesPorRuaId.Count;
|
||||
|
||||
|
||||
public MapaRenderResult(
|
||||
Layer layer,
|
||||
Dictionary<string, GeometryFeature> featuresPorRuaId)
|
||||
{
|
||||
Layer =
|
||||
layer
|
||||
?? throw new ArgumentNullException(nameof(layer));
|
||||
|
||||
FeaturesPorRuaId =
|
||||
featuresPorRuaId
|
||||
?? throw new ArgumentNullException(nameof(featuresPorRuaId));
|
||||
}
|
||||
|
||||
|
||||
public GeometryFeature? ObterFeature(
|
||||
string ruaId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ruaId))
|
||||
return null;
|
||||
|
||||
|
||||
return FeaturesPorRuaId.TryGetValue(
|
||||
ruaId.Trim(),
|
||||
out GeometryFeature? feature
|
||||
)
|
||||
? feature
|
||||
: null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,417 @@
|
|||
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
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,275 @@
|
|||
using Mapsui;
|
||||
using Mapsui.Layers;
|
||||
using Mapsui.Nts;
|
||||
using Mapsui.Styles;
|
||||
using NetTopologySuite.Geometries;
|
||||
using OperationControl.Mapas.Models;
|
||||
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>
|
||||
/// Representação visual da rota manual de retorno.
|
||||
///
|
||||
/// Não possui estado operacional.
|
||||
/// A fonte da verdade continua sendo MapaRotaRetorno.
|
||||
/// </summary>
|
||||
public sealed class MapaRotaRetornoRenderService
|
||||
{
|
||||
public const string NomeCamada =
|
||||
"MapaRotaRetornoNovo";
|
||||
|
||||
|
||||
public MemoryLayer CriarCamada(
|
||||
MapaRotaRetorno rota)
|
||||
{
|
||||
if (rota == null)
|
||||
{
|
||||
throw new ArgumentNullException(
|
||||
nameof(rota)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return new MemoryLayer
|
||||
{
|
||||
Name = NomeCamada,
|
||||
Features = CriarFeatures(rota)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public void AtualizarCamada(
|
||||
MemoryLayer camada,
|
||||
MapaRotaRetorno rota)
|
||||
{
|
||||
if (camada == null)
|
||||
{
|
||||
throw new ArgumentNullException(
|
||||
nameof(camada)
|
||||
);
|
||||
}
|
||||
|
||||
if (rota == null)
|
||||
{
|
||||
throw new ArgumentNullException(
|
||||
nameof(rota)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
camada.Features =
|
||||
CriarFeatures(
|
||||
rota
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private static IEnumerable<IFeature>
|
||||
CriarFeatures(
|
||||
MapaRotaRetorno rota)
|
||||
{
|
||||
var features =
|
||||
new List<IFeature>();
|
||||
|
||||
|
||||
/*
|
||||
* Primeiro desenhamos a linha da rota.
|
||||
*
|
||||
* A linha só existe visualmente.
|
||||
* O domínio continua sendo apenas a sequência
|
||||
* de MapaPontoRetorno.
|
||||
*/
|
||||
if (rota.Pontos.Count >= 2)
|
||||
{
|
||||
var coordenadas =
|
||||
rota.Pontos
|
||||
.Select(
|
||||
ponto =>
|
||||
{
|
||||
var mercator =
|
||||
Mapsui.Projections
|
||||
.SphericalMercator
|
||||
.FromLonLat(
|
||||
ponto.Longitude,
|
||||
ponto.Latitude
|
||||
);
|
||||
|
||||
return new Coordinate(
|
||||
mercator.x,
|
||||
mercator.y
|
||||
);
|
||||
}
|
||||
)
|
||||
.ToArray();
|
||||
|
||||
|
||||
var linha =
|
||||
new LineString(
|
||||
coordenadas
|
||||
);
|
||||
|
||||
|
||||
var featureLinha =
|
||||
new GeometryFeature
|
||||
{
|
||||
Geometry = linha
|
||||
};
|
||||
|
||||
|
||||
featureLinha["Tipo"] =
|
||||
"RotaRetorno";
|
||||
|
||||
|
||||
featureLinha.Styles.Add(
|
||||
CriarEstiloLinha()
|
||||
);
|
||||
|
||||
|
||||
features.Add(
|
||||
featureLinha
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Depois entram os pontos numerados.
|
||||
*
|
||||
* Como são adicionados depois da linha,
|
||||
* ficam visualmente por cima dela.
|
||||
*/
|
||||
for (
|
||||
int i = 0;
|
||||
i < rota.Pontos.Count;
|
||||
i++)
|
||||
{
|
||||
MapaPontoRetorno ponto =
|
||||
rota.Pontos[i];
|
||||
|
||||
|
||||
var mercator =
|
||||
Mapsui.Projections
|
||||
.SphericalMercator
|
||||
.FromLonLat(
|
||||
ponto.Longitude,
|
||||
ponto.Latitude
|
||||
);
|
||||
|
||||
|
||||
var feature =
|
||||
new PointFeature(
|
||||
mercator.x,
|
||||
mercator.y
|
||||
);
|
||||
|
||||
|
||||
feature["Tipo"] =
|
||||
"PontoRetorno";
|
||||
|
||||
feature["Ordem"] =
|
||||
i + 1;
|
||||
|
||||
feature["label"] =
|
||||
(i + 1).ToString();
|
||||
|
||||
|
||||
feature.Styles.Add(
|
||||
CriarEstiloPonto()
|
||||
);
|
||||
|
||||
feature.Styles.Add(
|
||||
CriarEstiloLabel()
|
||||
);
|
||||
|
||||
|
||||
features.Add(
|
||||
feature
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return features;
|
||||
}
|
||||
|
||||
|
||||
private static VectorStyle
|
||||
CriarEstiloLinha()
|
||||
{
|
||||
return new VectorStyle
|
||||
{
|
||||
Line =
|
||||
new Pen(
|
||||
Color.Red,
|
||||
3
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private static SymbolStyle
|
||||
CriarEstiloPonto()
|
||||
{
|
||||
return new SymbolStyle
|
||||
{
|
||||
SymbolType =
|
||||
SymbolType.Ellipse,
|
||||
|
||||
SymbolScale =
|
||||
1.2,
|
||||
|
||||
Fill =
|
||||
new Brush(
|
||||
Color.Red
|
||||
),
|
||||
|
||||
Outline =
|
||||
new Pen(
|
||||
Color.White,
|
||||
2
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private static LabelStyle
|
||||
CriarEstiloLabel()
|
||||
{
|
||||
return new LabelStyle
|
||||
{
|
||||
LabelColumn =
|
||||
"label",
|
||||
|
||||
ForeColor =
|
||||
Color.White,
|
||||
|
||||
BackColor =
|
||||
null,
|
||||
|
||||
HorizontalAlignment =
|
||||
LabelStyle
|
||||
.HorizontalAlignmentEnum
|
||||
.Center,
|
||||
|
||||
VerticalAlignment =
|
||||
LabelStyle
|
||||
.VerticalAlignmentEnum
|
||||
.Center,
|
||||
|
||||
Offset =
|
||||
new Offset(
|
||||
0,
|
||||
0
|
||||
),
|
||||
|
||||
Font =
|
||||
new Font
|
||||
{
|
||||
Size = 12
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,318 @@
|
|||
using AgroBase.Models;
|
||||
using NetTopologySuite.Geometries;
|
||||
using OperationControl.Mapas.Models;
|
||||
using static AgroBase.Models.Enums;
|
||||
|
||||
namespace OperationControl.Mapas.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Reconstrói um MapaDocumento a partir do contrato
|
||||
/// operacional utilizado na comunicação base/rover.
|
||||
///
|
||||
/// Este serviço não lê arquivos físicos.
|
||||
/// </summary>
|
||||
public sealed class MapaContratoImportService
|
||||
{
|
||||
private readonly GeometryFactory _geometryFactory =
|
||||
new GeometryFactory(
|
||||
new PrecisionModel(),
|
||||
4326
|
||||
);
|
||||
|
||||
private readonly MapaValidationService
|
||||
_validationService;
|
||||
|
||||
|
||||
public MapaContratoImportService(
|
||||
MapaValidationService? validationService = null)
|
||||
{
|
||||
_validationService =
|
||||
validationService
|
||||
?? new MapaValidationService();
|
||||
}
|
||||
|
||||
|
||||
public MapaDocumento Importar(
|
||||
MapaFeatureCollectionModel mapa,
|
||||
TipoMapaOperacao tipo,
|
||||
string nome = "Mapa operacional recebido")
|
||||
{
|
||||
if (mapa == null)
|
||||
{
|
||||
throw new ArgumentNullException(
|
||||
nameof(mapa)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (mapa.features == null ||
|
||||
mapa.features.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"O mapa recebido não possui ruas."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
var documento =
|
||||
new MapaDocumento
|
||||
{
|
||||
Nome =
|
||||
string.IsNullOrWhiteSpace(nome)
|
||||
? "Mapa operacional recebido"
|
||||
: nome.Trim(),
|
||||
|
||||
CaminhoOrigem = null,
|
||||
|
||||
FormatoOrigem =
|
||||
MapaFormatoOrigem
|
||||
.ContratoOperacional,
|
||||
|
||||
Tipo = tipo,
|
||||
|
||||
Modificado = false
|
||||
};
|
||||
|
||||
|
||||
var idsUsados =
|
||||
new HashSet<string>(
|
||||
StringComparer.OrdinalIgnoreCase
|
||||
);
|
||||
|
||||
|
||||
for (
|
||||
int i = 0;
|
||||
i < mapa.features.Count;
|
||||
i++)
|
||||
{
|
||||
var feature =
|
||||
mapa.features[i];
|
||||
|
||||
|
||||
if (feature == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"A feature {i + 1} é nula."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (feature.geometry == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"A feature {i + 1} não possui geometria."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (!string.Equals(
|
||||
feature.geometry.type,
|
||||
"LineString",
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"A feature {i + 1} não é uma LineString."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (feature.geometry.coordinates == null ||
|
||||
feature.geometry.coordinates.Count < 2)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"A feature {i + 1} não possui pontos suficientes."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* IMPORTANTE:
|
||||
*
|
||||
* properties.Id é autoritativo.
|
||||
*
|
||||
* O fluxo legado já produziu mapas onde
|
||||
* geometry.id podia divergir de properties.Id.
|
||||
*
|
||||
* RuasPercorrer e a trajetória do rover trabalham
|
||||
* pelo properties.Id.
|
||||
*/
|
||||
string? idProperties =
|
||||
feature.properties?
|
||||
.Id?
|
||||
.Trim();
|
||||
|
||||
|
||||
string? idGeometry =
|
||||
feature.geometry?
|
||||
.id?
|
||||
.Trim();
|
||||
|
||||
|
||||
string? id =
|
||||
!string.IsNullOrWhiteSpace(
|
||||
idProperties)
|
||||
? idProperties
|
||||
: idGeometry;
|
||||
|
||||
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"A feature {i + 1} não possui ID."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (
|
||||
!string.IsNullOrWhiteSpace(idProperties) &&
|
||||
!string.IsNullOrWhiteSpace(idGeometry) &&
|
||||
!string.Equals(
|
||||
idProperties,
|
||||
idGeometry,
|
||||
StringComparison.OrdinalIgnoreCase
|
||||
)
|
||||
)
|
||||
{
|
||||
documento.AvisosImportacao.Add(
|
||||
$"Feature {i + 1}: " +
|
||||
$"properties.Id='{idProperties}' e " +
|
||||
$"geometry.id='{idGeometry}' divergem. " +
|
||||
$"Foi utilizado properties.Id."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (!idsUsados.Add(id))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"O mapa recebido possui o ID '{id}' duplicado."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
var coordenadas =
|
||||
new List<Coordinate>();
|
||||
|
||||
|
||||
for (
|
||||
int p = 0;
|
||||
p <
|
||||
feature.geometry.coordinates.Count;
|
||||
p++)
|
||||
{
|
||||
var ponto =
|
||||
feature.geometry.coordinates[p];
|
||||
|
||||
|
||||
if (ponto == null ||
|
||||
ponto.Count < 2)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"A feature '{id}', ponto {p + 1}, " +
|
||||
"possui coordenada incompleta."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
double longitude =
|
||||
ponto[0];
|
||||
|
||||
double latitude =
|
||||
ponto[1];
|
||||
|
||||
|
||||
if (
|
||||
!double.IsFinite(longitude) ||
|
||||
!double.IsFinite(latitude) ||
|
||||
longitude < -180.0 ||
|
||||
longitude > 180.0 ||
|
||||
latitude < -90.0 ||
|
||||
latitude > 90.0
|
||||
)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"A feature '{id}', ponto {p + 1}, " +
|
||||
"possui coordenada WGS84 inválida."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
coordenadas.Add(
|
||||
new Coordinate(
|
||||
longitude,
|
||||
latitude
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
LineString linha =
|
||||
_geometryFactory
|
||||
.CreateLineString(
|
||||
coordenadas.ToArray()
|
||||
);
|
||||
|
||||
|
||||
documento.Ruas.Add(
|
||||
new MapaRua
|
||||
{
|
||||
Id = id,
|
||||
|
||||
IdOriginal = id,
|
||||
|
||||
IdGeradoAutomaticamente =
|
||||
false,
|
||||
|
||||
Nome =
|
||||
string.IsNullOrWhiteSpace(
|
||||
feature.properties?.Name)
|
||||
? $"Rua {id}"
|
||||
: feature.properties.Name,
|
||||
|
||||
Geometria =
|
||||
linha,
|
||||
|
||||
/*
|
||||
* Contrato já enviado pelo próprio sistema.
|
||||
* Posteriormente o GeometryService vai
|
||||
* centralizar este cálculo.
|
||||
*/
|
||||
ComprimentoMetros =
|
||||
feature.properties?.Length ?? 0.0,
|
||||
|
||||
Dist1 =
|
||||
feature.properties?.Dist1 ?? 0.0,
|
||||
|
||||
Dist2 =
|
||||
feature.properties?.Dist2 ?? 0.0,
|
||||
|
||||
IndiceFeatureOrigem =
|
||||
i + 1
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
var validacao =
|
||||
_validationService.Validar(
|
||||
documento
|
||||
);
|
||||
|
||||
|
||||
if (!validacao.Valido)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"O mapa operacional recebido é inválido:" +
|
||||
Environment.NewLine +
|
||||
string.Join(
|
||||
Environment.NewLine,
|
||||
validacao.Erros
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return documento;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
using OperationControl.Mapas.Models;
|
||||
|
||||
namespace OperationControl.Mapas.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Responsável por criar cópias profundas e independentes
|
||||
/// de documentos canônicos do subsistema de mapas.
|
||||
///
|
||||
/// Nenhuma coleção, MapaRua ou geometria mutável é
|
||||
/// compartilhada entre origem e clone.
|
||||
/// </summary>
|
||||
public sealed class MapaDocumentoCloneService
|
||||
{
|
||||
public MapaDocumento Clonar(
|
||||
MapaDocumento origem)
|
||||
{
|
||||
if (origem == null)
|
||||
{
|
||||
throw new ArgumentNullException(
|
||||
nameof(origem)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
var clone =
|
||||
new MapaDocumento
|
||||
{
|
||||
Nome =
|
||||
origem.Nome,
|
||||
|
||||
CaminhoOrigem =
|
||||
origem.CaminhoOrigem,
|
||||
|
||||
FormatoOrigem =
|
||||
origem.FormatoOrigem,
|
||||
|
||||
Tipo =
|
||||
origem.Tipo,
|
||||
|
||||
Modificado =
|
||||
origem.Modificado
|
||||
};
|
||||
|
||||
|
||||
if (origem.AvisosImportacao != null)
|
||||
{
|
||||
clone
|
||||
.AvisosImportacao
|
||||
.AddRange(
|
||||
origem.AvisosImportacao
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (origem.Ruas == null)
|
||||
return clone;
|
||||
|
||||
|
||||
foreach (
|
||||
var rua
|
||||
in origem.Ruas)
|
||||
{
|
||||
if (rua == null)
|
||||
continue;
|
||||
|
||||
|
||||
clone.Ruas.Add(
|
||||
new MapaRua
|
||||
{
|
||||
Id =
|
||||
rua.Id,
|
||||
|
||||
IdOriginal =
|
||||
rua.IdOriginal,
|
||||
|
||||
IdGeradoAutomaticamente =
|
||||
rua.IdGeradoAutomaticamente,
|
||||
|
||||
Nome =
|
||||
rua.Nome,
|
||||
|
||||
Geometria =
|
||||
rua.Geometria == null
|
||||
? null
|
||||
: (NetTopologySuite
|
||||
.Geometries
|
||||
.LineString)
|
||||
rua.Geometria
|
||||
.Copy(),
|
||||
|
||||
ComprimentoMetros =
|
||||
rua.ComprimentoMetros,
|
||||
|
||||
Dist1 =
|
||||
rua.Dist1,
|
||||
|
||||
Dist2 =
|
||||
rua.Dist2,
|
||||
|
||||
IndiceFeatureOrigem =
|
||||
rua.IndiceFeatureOrigem
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,419 @@
|
|||
using OperationControl.Mapas.Models;
|
||||
using static AgroBase.Models.Enums;
|
||||
|
||||
namespace OperationControl.Mapas.Services
|
||||
{
|
||||
public sealed class MapaEntidadeDinamicaService
|
||||
{
|
||||
public MapaEntidadeDinamica DefinirBase(
|
||||
MapaEstadoDinamico estado,
|
||||
string id,
|
||||
string nome,
|
||||
double? latitude,
|
||||
double? longitude,
|
||||
double? heading = null,
|
||||
double? latitudeBruta = null,
|
||||
double? longitudeBruta = null)
|
||||
{
|
||||
return DefinirEntidade(
|
||||
estado,
|
||||
id,
|
||||
MapaTipoEntidadeDinamica.Base,
|
||||
nome,
|
||||
latitude,
|
||||
longitude,
|
||||
heading,
|
||||
latitudeBruta,
|
||||
longitudeBruta
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public MapaEntidadeDinamica DefinirRover(
|
||||
MapaEstadoDinamico estado,
|
||||
string id,
|
||||
string nome,
|
||||
double? latitude,
|
||||
double? longitude,
|
||||
double? heading = null,
|
||||
double? latitudeBruta = null,
|
||||
double? longitudeBruta = null)
|
||||
{
|
||||
return DefinirEntidade(
|
||||
estado,
|
||||
id,
|
||||
MapaTipoEntidadeDinamica.Rover,
|
||||
nome,
|
||||
latitude,
|
||||
longitude,
|
||||
heading,
|
||||
latitudeBruta,
|
||||
longitudeBruta
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public void AtualizarPosicao(
|
||||
MapaEstadoDinamico estado,
|
||||
string id,
|
||||
double? latitude,
|
||||
double? longitude,
|
||||
double? heading = null,
|
||||
double? latitudeBruta = null,
|
||||
double? longitudeBruta = null)
|
||||
{
|
||||
var entidade =
|
||||
ObterObrigatoria(
|
||||
estado,
|
||||
id
|
||||
);
|
||||
|
||||
|
||||
if (
|
||||
latitude.HasValue &&
|
||||
longitude.HasValue
|
||||
)
|
||||
{
|
||||
ValidarCoordenada(
|
||||
latitude.Value,
|
||||
longitude.Value
|
||||
);
|
||||
|
||||
entidade.Latitude =
|
||||
latitude.Value;
|
||||
|
||||
entidade.Longitude =
|
||||
longitude.Value;
|
||||
}
|
||||
|
||||
|
||||
if (
|
||||
heading.HasValue &&
|
||||
double.IsFinite(
|
||||
heading.Value
|
||||
)
|
||||
)
|
||||
{
|
||||
entidade.Heading =
|
||||
NormalizarHeading(
|
||||
heading.Value
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (
|
||||
latitudeBruta.HasValue &&
|
||||
longitudeBruta.HasValue
|
||||
)
|
||||
{
|
||||
ValidarCoordenada(
|
||||
latitudeBruta.Value,
|
||||
longitudeBruta.Value
|
||||
);
|
||||
|
||||
entidade.LatitudeBruta =
|
||||
latitudeBruta.Value;
|
||||
|
||||
entidade.LongitudeBruta =
|
||||
longitudeBruta.Value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void AtualizarStatus(
|
||||
MapaEstadoDinamico estado,
|
||||
string id,
|
||||
StatusOperacao status,
|
||||
bool? pulverizando = null)
|
||||
{
|
||||
var entidade =
|
||||
ObterObrigatoria(
|
||||
estado,
|
||||
id
|
||||
);
|
||||
|
||||
|
||||
entidade.Status =
|
||||
status;
|
||||
|
||||
|
||||
if (pulverizando.HasValue)
|
||||
{
|
||||
entidade.Pulverizando =
|
||||
pulverizando.Value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void DefinirFoco(
|
||||
MapaEstadoDinamico estado,
|
||||
string? id)
|
||||
{
|
||||
ValidarEstado(
|
||||
estado
|
||||
);
|
||||
|
||||
|
||||
foreach (
|
||||
var entidade
|
||||
in estado.EntidadesMutaveis.Values)
|
||||
{
|
||||
entidade.EmFoco =
|
||||
false;
|
||||
}
|
||||
|
||||
|
||||
if (
|
||||
string.IsNullOrWhiteSpace(id)
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (
|
||||
estado.EntidadesMutaveis.TryGetValue(
|
||||
id.Trim(),
|
||||
out var selecionada
|
||||
)
|
||||
)
|
||||
{
|
||||
selecionada.EmFoco =
|
||||
true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public MapaEntidadeDinamica?
|
||||
Obter(
|
||||
MapaEstadoDinamico estado,
|
||||
string id)
|
||||
{
|
||||
ValidarEstado(
|
||||
estado
|
||||
);
|
||||
|
||||
|
||||
if (
|
||||
string.IsNullOrWhiteSpace(id)
|
||||
)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
estado.EntidadesMutaveis
|
||||
.TryGetValue(
|
||||
id.Trim(),
|
||||
out var entidade
|
||||
);
|
||||
|
||||
|
||||
return entidade;
|
||||
}
|
||||
|
||||
|
||||
public bool Remover(
|
||||
MapaEstadoDinamico estado,
|
||||
string id)
|
||||
{
|
||||
ValidarEstado(
|
||||
estado
|
||||
);
|
||||
|
||||
|
||||
if (
|
||||
string.IsNullOrWhiteSpace(id)
|
||||
)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
return estado
|
||||
.EntidadesMutaveis
|
||||
.Remove(
|
||||
id.Trim()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private MapaEntidadeDinamica
|
||||
DefinirEntidade(
|
||||
MapaEstadoDinamico estado,
|
||||
string id,
|
||||
MapaTipoEntidadeDinamica tipo,
|
||||
string nome,
|
||||
double? latitude,
|
||||
double? longitude,
|
||||
double? heading,
|
||||
double? latitudeBruta,
|
||||
double? longitudeBruta)
|
||||
{
|
||||
ValidarEstado(
|
||||
estado
|
||||
);
|
||||
|
||||
|
||||
if (
|
||||
string.IsNullOrWhiteSpace(id)
|
||||
)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"O ID da entidade não foi informado.",
|
||||
nameof(id)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
string idCanonico =
|
||||
id.Trim();
|
||||
|
||||
|
||||
if (
|
||||
!estado.EntidadesMutaveis
|
||||
.TryGetValue(
|
||||
idCanonico,
|
||||
out var entidade
|
||||
)
|
||||
)
|
||||
{
|
||||
entidade =
|
||||
new MapaEntidadeDinamica
|
||||
{
|
||||
Id =
|
||||
idCanonico
|
||||
};
|
||||
|
||||
|
||||
estado.EntidadesMutaveis[
|
||||
idCanonico
|
||||
] =
|
||||
entidade;
|
||||
}
|
||||
|
||||
|
||||
entidade.Tipo =
|
||||
tipo;
|
||||
|
||||
entidade.Nome =
|
||||
string.IsNullOrWhiteSpace(nome)
|
||||
? idCanonico
|
||||
: nome.Trim();
|
||||
|
||||
|
||||
AtualizarPosicao(
|
||||
estado,
|
||||
idCanonico,
|
||||
latitude,
|
||||
longitude,
|
||||
heading,
|
||||
latitudeBruta,
|
||||
longitudeBruta
|
||||
);
|
||||
|
||||
|
||||
return entidade;
|
||||
}
|
||||
|
||||
|
||||
private static MapaEntidadeDinamica
|
||||
ObterObrigatoria(
|
||||
MapaEstadoDinamico estado,
|
||||
string id)
|
||||
{
|
||||
ValidarEstado(
|
||||
estado
|
||||
);
|
||||
|
||||
|
||||
if (
|
||||
string.IsNullOrWhiteSpace(id)
|
||||
)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"O ID da entidade não foi informado.",
|
||||
nameof(id)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (
|
||||
!estado.EntidadesMutaveis
|
||||
.TryGetValue(
|
||||
id.Trim(),
|
||||
out var entidade
|
||||
)
|
||||
)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"A entidade '{id}' não existe."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return entidade;
|
||||
}
|
||||
|
||||
|
||||
private static void ValidarCoordenada(
|
||||
double latitude,
|
||||
double longitude)
|
||||
{
|
||||
if (
|
||||
!double.IsFinite(latitude) ||
|
||||
latitude < -90.0 ||
|
||||
latitude > 90.0
|
||||
)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(latitude),
|
||||
"Latitude WGS84 inválida."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (
|
||||
!double.IsFinite(longitude) ||
|
||||
longitude < -180.0 ||
|
||||
longitude > 180.0
|
||||
)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(longitude),
|
||||
"Longitude WGS84 inválida."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static double NormalizarHeading(
|
||||
double heading)
|
||||
{
|
||||
double normalizado =
|
||||
heading % 360.0;
|
||||
|
||||
|
||||
if (normalizado < 0)
|
||||
{
|
||||
normalizado +=
|
||||
360.0;
|
||||
}
|
||||
|
||||
|
||||
return normalizado;
|
||||
}
|
||||
|
||||
|
||||
private static void ValidarEstado(
|
||||
MapaEstadoDinamico estado)
|
||||
{
|
||||
if (estado == null)
|
||||
{
|
||||
throw new ArgumentNullException(
|
||||
nameof(estado)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,752 @@
|
|||
using NetTopologySuite.Geometries;
|
||||
using NetTopologySuite.LinearReferencing;
|
||||
using OperationControl.Mapas.Models;
|
||||
|
||||
namespace OperationControl.Mapas.Services
|
||||
{
|
||||
public sealed class MapaEstimativaOperacaoService
|
||||
{
|
||||
/*
|
||||
* 21 pontos distribuídos na parte útil
|
||||
* da rua dão uma leitura boa do espaçamento,
|
||||
* sem transformar cada clique em um processamento GIS pesado.
|
||||
*/
|
||||
private const int QuantidadeAmostrasLargura = 21;
|
||||
|
||||
|
||||
/*
|
||||
* Cache válido enquanto o Documento oficial
|
||||
* continuar sendo a mesma instância.
|
||||
*/
|
||||
private MapaDocumento?
|
||||
_documentoCache;
|
||||
|
||||
|
||||
private readonly Dictionary<
|
||||
string,
|
||||
MapaEstimativaCorredor>
|
||||
_cacheCorredores =
|
||||
new(
|
||||
StringComparer.OrdinalIgnoreCase
|
||||
);
|
||||
|
||||
|
||||
public MapaEstimativaOperacao Calcular(
|
||||
MapaDocumento? documento,
|
||||
MapaSelecaoOperacao? selecao,
|
||||
double? velocidadeSemErvasMs = null,
|
||||
double? velocidadeComErvasMs = null,
|
||||
double percentualErvas = 0)
|
||||
{
|
||||
int quantidadeSelecionadas =
|
||||
selecao?.Quantidade ?? 0;
|
||||
|
||||
|
||||
var resultado =
|
||||
new MapaEstimativaOperacao
|
||||
{
|
||||
QuantidadeRuasSelecionadas =
|
||||
quantidadeSelecionadas,
|
||||
|
||||
PercentualErvasConsiderado =
|
||||
Math.Clamp(
|
||||
percentualErvas,
|
||||
0,
|
||||
100
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
if (
|
||||
documento == null ||
|
||||
selecao == null ||
|
||||
quantidadeSelecionadas < 2
|
||||
)
|
||||
{
|
||||
resultado.MotivoIndisponivel =
|
||||
"Selecione ao menos duas ruas.";
|
||||
|
||||
return resultado;
|
||||
}
|
||||
|
||||
|
||||
GarantirCacheDocumento(
|
||||
documento
|
||||
);
|
||||
|
||||
|
||||
var ruasPorId =
|
||||
documento
|
||||
.Ruas
|
||||
.Where(
|
||||
x =>
|
||||
x != null &&
|
||||
!string.IsNullOrWhiteSpace(
|
||||
x.Id
|
||||
)
|
||||
)
|
||||
.ToDictionary(
|
||||
x => x.Id,
|
||||
x => x,
|
||||
StringComparer.OrdinalIgnoreCase
|
||||
);
|
||||
|
||||
|
||||
/*
|
||||
* A ordem da seleção é a ordem operacional.
|
||||
*
|
||||
* Rua 0 + Rua 1 = corredor 0
|
||||
* Rua 1 + Rua 2 = corredor 1
|
||||
* etc.
|
||||
*/
|
||||
var ids =
|
||||
selecao
|
||||
.RuasOrdenadas
|
||||
.ToList();
|
||||
|
||||
|
||||
for (
|
||||
int i = 0;
|
||||
i < ids.Count - 1;
|
||||
i++)
|
||||
{
|
||||
string idA =
|
||||
ids[i];
|
||||
|
||||
string idB =
|
||||
ids[i + 1];
|
||||
|
||||
|
||||
if (
|
||||
!ruasPorId.TryGetValue(
|
||||
idA,
|
||||
out var ruaA
|
||||
) ||
|
||||
!ruasPorId.TryGetValue(
|
||||
idB,
|
||||
out var ruaB
|
||||
)
|
||||
)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Não foi possível montar " +
|
||||
$"o corredor {idA} -> {idB}."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
var corredor =
|
||||
ObterCorredor(
|
||||
documento,
|
||||
ruaA,
|
||||
ruaB
|
||||
);
|
||||
|
||||
|
||||
resultado
|
||||
.Corredores
|
||||
.Add(
|
||||
corredor
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
resultado.DistanciaMetros =
|
||||
resultado
|
||||
.Corredores
|
||||
.Sum(
|
||||
x =>
|
||||
x.ComprimentoMetros
|
||||
);
|
||||
|
||||
|
||||
resultado.AreaMetrosQuadrados =
|
||||
resultado
|
||||
.Corredores
|
||||
.Sum(
|
||||
x =>
|
||||
x.AreaMetrosQuadrados
|
||||
);
|
||||
|
||||
|
||||
resultado.LarguraMediaMetros =
|
||||
resultado.Corredores.Count > 0
|
||||
? resultado
|
||||
.Corredores
|
||||
.Average(
|
||||
x =>
|
||||
x.LarguraMetros
|
||||
)
|
||||
: 0;
|
||||
|
||||
|
||||
resultado.TempoEstimadoSegundos =
|
||||
CalcularTempoEstimado(
|
||||
resultado.DistanciaMetros,
|
||||
velocidadeSemErvasMs,
|
||||
velocidadeComErvasMs,
|
||||
resultado
|
||||
.PercentualErvasConsiderado
|
||||
);
|
||||
|
||||
|
||||
return resultado;
|
||||
}
|
||||
|
||||
|
||||
private void GarantirCacheDocumento(
|
||||
MapaDocumento documento)
|
||||
{
|
||||
if (
|
||||
ReferenceEquals(
|
||||
_documentoCache,
|
||||
documento
|
||||
)
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
_documentoCache =
|
||||
documento;
|
||||
|
||||
|
||||
_cacheCorredores.Clear();
|
||||
}
|
||||
|
||||
|
||||
private MapaEstimativaCorredor
|
||||
ObterCorredor(
|
||||
MapaDocumento documento,
|
||||
MapaRua ruaA,
|
||||
MapaRua ruaB)
|
||||
{
|
||||
string chave =
|
||||
CriarChave(
|
||||
ruaA.Id,
|
||||
ruaB.Id
|
||||
);
|
||||
|
||||
|
||||
if (
|
||||
_cacheCorredores
|
||||
.TryGetValue(
|
||||
chave,
|
||||
out var existente
|
||||
)
|
||||
)
|
||||
{
|
||||
return existente;
|
||||
}
|
||||
|
||||
|
||||
var calculado =
|
||||
CalcularCorredor(
|
||||
ruaA,
|
||||
ruaB
|
||||
);
|
||||
|
||||
|
||||
_cacheCorredores[
|
||||
chave
|
||||
] =
|
||||
calculado;
|
||||
|
||||
|
||||
return calculado;
|
||||
}
|
||||
|
||||
|
||||
private static string CriarChave(
|
||||
string idA,
|
||||
string idB)
|
||||
{
|
||||
return string.Compare(
|
||||
idA,
|
||||
idB,
|
||||
StringComparison.OrdinalIgnoreCase
|
||||
) <= 0
|
||||
? $"{idA}\u001F{idB}"
|
||||
: $"{idB}\u001F{idA}";
|
||||
}
|
||||
|
||||
|
||||
private static MapaEstimativaCorredor
|
||||
CalcularCorredor(
|
||||
MapaRua ruaA,
|
||||
MapaRua ruaB)
|
||||
{
|
||||
if (
|
||||
ruaA?.Geometria == null ||
|
||||
ruaB?.Geometria == null ||
|
||||
ruaA.Geometria.IsEmpty ||
|
||||
ruaB.Geometria.IsEmpty ||
|
||||
ruaA.Geometria.NumPoints < 2 ||
|
||||
ruaB.Geometria.NumPoints < 2
|
||||
)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Uma das ruas possui geometria inválida."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
var metricas =
|
||||
ProjetarParParaMetros(
|
||||
ruaA.Geometria,
|
||||
ruaB.Geometria
|
||||
);
|
||||
|
||||
|
||||
double comprimentoA =
|
||||
ValorComprimentoValido(
|
||||
ruaA.ComprimentoMetros
|
||||
)
|
||||
? ruaA.ComprimentoMetros
|
||||
: metricas.A.Length;
|
||||
|
||||
|
||||
double comprimentoB =
|
||||
ValorComprimentoValido(
|
||||
ruaB.ComprimentoMetros
|
||||
)
|
||||
? ruaB.ComprimentoMetros
|
||||
: metricas.B.Length;
|
||||
|
||||
|
||||
/*
|
||||
* Regra operacional definida:
|
||||
*
|
||||
* corredor usa o comprimento da MAIOR rua,
|
||||
* garantindo que a estimativa cubra completamente
|
||||
* as duas laterais.
|
||||
*/
|
||||
double comprimentoCorredor =
|
||||
Math.Max(
|
||||
comprimentoA,
|
||||
comprimentoB
|
||||
);
|
||||
|
||||
|
||||
double largura =
|
||||
CalcularLarguraMedia(
|
||||
metricas.A,
|
||||
metricas.B
|
||||
);
|
||||
|
||||
|
||||
if (
|
||||
!double.IsFinite(largura) ||
|
||||
largura <= 0
|
||||
)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Não foi possível calcular a largura " +
|
||||
$"entre as ruas {ruaA.Id} e {ruaB.Id}."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return new MapaEstimativaCorredor
|
||||
{
|
||||
RuaAId =
|
||||
ruaA.Id,
|
||||
|
||||
RuaBId =
|
||||
ruaB.Id,
|
||||
|
||||
ComprimentoMetros =
|
||||
comprimentoCorredor,
|
||||
|
||||
LarguraMetros =
|
||||
largura
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private static bool ValorComprimentoValido(
|
||||
double valor)
|
||||
{
|
||||
return
|
||||
double.IsFinite(valor) &&
|
||||
valor > 0;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* O documento permanece WGS84.
|
||||
*
|
||||
* Criamos uma projeção cartesiana LOCAL apenas
|
||||
* para medições métricas.
|
||||
*
|
||||
* Para a escala de um talhão agrícola,
|
||||
* a aproximação é mais do que suficiente.
|
||||
*/
|
||||
private static (
|
||||
LineString A,
|
||||
LineString B)
|
||||
ProjetarParParaMetros(
|
||||
LineString linhaA,
|
||||
LineString linhaB)
|
||||
{
|
||||
var envelope =
|
||||
new Envelope(
|
||||
linhaA.EnvelopeInternal
|
||||
);
|
||||
|
||||
|
||||
envelope.ExpandToInclude(
|
||||
linhaB.EnvelopeInternal
|
||||
);
|
||||
|
||||
|
||||
double lonReferencia =
|
||||
(
|
||||
envelope.MinX +
|
||||
envelope.MaxX
|
||||
) /
|
||||
2.0;
|
||||
|
||||
|
||||
double latReferencia =
|
||||
(
|
||||
envelope.MinY +
|
||||
envelope.MaxY
|
||||
) /
|
||||
2.0;
|
||||
|
||||
|
||||
double latRad =
|
||||
latReferencia *
|
||||
Math.PI /
|
||||
180.0;
|
||||
|
||||
|
||||
const double metrosPorGrauLat =
|
||||
110540.0;
|
||||
|
||||
|
||||
double metrosPorGrauLon =
|
||||
111320.0 *
|
||||
Math.Cos(
|
||||
latRad
|
||||
);
|
||||
|
||||
|
||||
var factory =
|
||||
new GeometryFactory(
|
||||
new PrecisionModel(),
|
||||
0
|
||||
);
|
||||
|
||||
|
||||
LineString Converter(
|
||||
LineString origem)
|
||||
{
|
||||
var coords =
|
||||
origem
|
||||
.Coordinates
|
||||
.Select(
|
||||
c =>
|
||||
new Coordinate(
|
||||
(
|
||||
c.X -
|
||||
lonReferencia
|
||||
) *
|
||||
metrosPorGrauLon,
|
||||
|
||||
(
|
||||
c.Y -
|
||||
latReferencia
|
||||
) *
|
||||
metrosPorGrauLat
|
||||
)
|
||||
)
|
||||
.ToArray();
|
||||
|
||||
|
||||
return factory
|
||||
.CreateLineString(
|
||||
coords
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
Converter(linhaA),
|
||||
Converter(linhaB)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private static double CalcularLarguraMedia(
|
||||
LineString linhaA,
|
||||
LineString linhaB)
|
||||
{
|
||||
var distancias =
|
||||
new List<double>();
|
||||
|
||||
|
||||
AdicionarAmostras(
|
||||
linhaA,
|
||||
linhaB,
|
||||
distancias
|
||||
);
|
||||
|
||||
|
||||
/*
|
||||
* Fazemos também B -> A.
|
||||
*
|
||||
* Assim a largura não muda simplesmente
|
||||
* porque o operador selecionou as ruas
|
||||
* na ordem inversa.
|
||||
*/
|
||||
AdicionarAmostras(
|
||||
linhaB,
|
||||
linhaA,
|
||||
distancias
|
||||
);
|
||||
|
||||
|
||||
distancias =
|
||||
distancias
|
||||
.Where(
|
||||
x =>
|
||||
double.IsFinite(x) &&
|
||||
x > 0
|
||||
)
|
||||
.OrderBy(
|
||||
x =>
|
||||
x
|
||||
)
|
||||
.ToList();
|
||||
|
||||
|
||||
if (distancias.Count == 0)
|
||||
return 0;
|
||||
|
||||
|
||||
/*
|
||||
* Média aparada de 10%.
|
||||
*
|
||||
* Pontinhas divergentes de rua, erros locais
|
||||
* e finais desencontrados não devem distorcer
|
||||
* a largura inteira do corredor.
|
||||
*/
|
||||
int remover =
|
||||
distancias.Count >= 10
|
||||
? (int)Math.Floor(
|
||||
distancias.Count *
|
||||
0.10
|
||||
)
|
||||
: 0;
|
||||
|
||||
|
||||
int quantidade =
|
||||
distancias.Count -
|
||||
remover * 2;
|
||||
|
||||
|
||||
if (quantidade <= 0)
|
||||
{
|
||||
return distancias.Average();
|
||||
}
|
||||
|
||||
|
||||
return distancias
|
||||
.Skip(remover)
|
||||
.Take(quantidade)
|
||||
.Average();
|
||||
}
|
||||
|
||||
|
||||
private static void AdicionarAmostras(
|
||||
LineString origem,
|
||||
LineString destino,
|
||||
List<double> resultado)
|
||||
{
|
||||
if (
|
||||
origem == null ||
|
||||
destino == null ||
|
||||
origem.Length <= 0 ||
|
||||
destino.Length <= 0
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var indexed =
|
||||
new LengthIndexedLine(
|
||||
origem
|
||||
);
|
||||
|
||||
|
||||
double inicio =
|
||||
indexed.StartIndex;
|
||||
|
||||
double fim =
|
||||
indexed.EndIndex;
|
||||
|
||||
|
||||
for (
|
||||
int i = 0;
|
||||
i < QuantidadeAmostrasLargura;
|
||||
i++)
|
||||
{
|
||||
/*
|
||||
* 5% -> 95%.
|
||||
*
|
||||
* Evita que diferenças apenas nas pontas
|
||||
* sejam interpretadas como largura lateral.
|
||||
*/
|
||||
double t =
|
||||
0.05 +
|
||||
(
|
||||
0.90 *
|
||||
i /
|
||||
(
|
||||
QuantidadeAmostrasLargura -
|
||||
1.0
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
double indice =
|
||||
inicio +
|
||||
(
|
||||
fim -
|
||||
inicio
|
||||
) *
|
||||
t;
|
||||
|
||||
|
||||
Coordinate coordenada =
|
||||
indexed.ExtractPoint(
|
||||
indice
|
||||
);
|
||||
|
||||
|
||||
var ponto =
|
||||
origem.Factory
|
||||
.CreatePoint(
|
||||
coordenada
|
||||
);
|
||||
|
||||
|
||||
double distancia =
|
||||
ponto.Distance(
|
||||
destino
|
||||
);
|
||||
|
||||
|
||||
if (
|
||||
double.IsFinite(
|
||||
distancia
|
||||
)
|
||||
)
|
||||
{
|
||||
resultado.Add(
|
||||
distancia
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static double?
|
||||
CalcularTempoEstimado(
|
||||
double distanciaMetros,
|
||||
double? velocidadeSemErvasMs,
|
||||
double? velocidadeComErvasMs,
|
||||
double percentualErvas)
|
||||
{
|
||||
if (
|
||||
!double.IsFinite(
|
||||
distanciaMetros
|
||||
) ||
|
||||
distanciaMetros <= 0
|
||||
)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
double fatorErvas =
|
||||
Math.Clamp(
|
||||
percentualErvas,
|
||||
0,
|
||||
100
|
||||
) /
|
||||
100.0;
|
||||
|
||||
|
||||
double fatorSemErvas =
|
||||
1.0 -
|
||||
fatorErvas;
|
||||
|
||||
|
||||
if (
|
||||
fatorSemErvas > 0 &&
|
||||
(
|
||||
!velocidadeSemErvasMs.HasValue ||
|
||||
!double.IsFinite(
|
||||
velocidadeSemErvasMs.Value
|
||||
) ||
|
||||
velocidadeSemErvasMs.Value <= 0
|
||||
)
|
||||
)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
if (
|
||||
fatorErvas > 0 &&
|
||||
(
|
||||
!velocidadeComErvasMs.HasValue ||
|
||||
!double.IsFinite(
|
||||
velocidadeComErvasMs.Value
|
||||
) ||
|
||||
velocidadeComErvasMs.Value <= 0
|
||||
)
|
||||
)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
double tempo =
|
||||
0;
|
||||
|
||||
|
||||
if (fatorSemErvas > 0)
|
||||
{
|
||||
tempo +=
|
||||
(
|
||||
distanciaMetros *
|
||||
fatorSemErvas
|
||||
) /
|
||||
velocidadeSemErvasMs!.Value;
|
||||
}
|
||||
|
||||
|
||||
if (fatorErvas > 0)
|
||||
{
|
||||
tempo +=
|
||||
(
|
||||
distanciaMetros *
|
||||
fatorErvas
|
||||
) /
|
||||
velocidadeComErvasMs!.Value;
|
||||
}
|
||||
|
||||
|
||||
return
|
||||
double.IsFinite(tempo)
|
||||
? tempo
|
||||
: null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
using AgroBase.Models;
|
||||
using OperationControl.Mapas.Models;
|
||||
|
||||
namespace OperationControl.Mapas.Services
|
||||
{
|
||||
public class MapaExportService
|
||||
{
|
||||
private readonly MapaValidationService
|
||||
_validationService;
|
||||
|
||||
|
||||
public MapaExportService(
|
||||
MapaValidationService? validationService = null)
|
||||
{
|
||||
_validationService =
|
||||
validationService
|
||||
?? new MapaValidationService();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Converte o documento canônico do Operation Control
|
||||
/// para o contrato atualmente consumido pelo rover.
|
||||
///
|
||||
/// Nenhuma referência geométrica do documento é compartilhada
|
||||
/// com o objeto exportado.
|
||||
/// </summary>
|
||||
public MapaFeatureCollectionModel
|
||||
CriarMapaOperacional(
|
||||
MapaDocumento documento)
|
||||
{
|
||||
MapaResultadoValidacao validacao =
|
||||
_validationService.Validar(
|
||||
documento
|
||||
);
|
||||
|
||||
|
||||
if (!validacao.Valido)
|
||||
{
|
||||
string erros =
|
||||
string.Join(
|
||||
Environment.NewLine,
|
||||
validacao.Erros
|
||||
);
|
||||
|
||||
|
||||
throw new InvalidOperationException(
|
||||
"O mapa não pode ser exportado porque " +
|
||||
"possui erros de validação:" +
|
||||
Environment.NewLine +
|
||||
erros
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
var mapa =
|
||||
new MapaFeatureCollectionModel
|
||||
{
|
||||
type =
|
||||
"FeatureCollection",
|
||||
|
||||
features =
|
||||
new List<MapaFeatureModel>()
|
||||
};
|
||||
|
||||
|
||||
foreach (
|
||||
MapaRua rua
|
||||
in documento.Ruas)
|
||||
{
|
||||
var coordenadas =
|
||||
new List<List<double>>();
|
||||
|
||||
|
||||
foreach (
|
||||
var coordenada
|
||||
in rua.Geometria.Coordinates)
|
||||
{
|
||||
/*
|
||||
* Contrato GeoJSON / rover:
|
||||
*
|
||||
* [longitude, latitude]
|
||||
*/
|
||||
coordenadas.Add(
|
||||
new List<double>
|
||||
{
|
||||
coordenada.X,
|
||||
coordenada.Y
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Esta igualdade é proposital.
|
||||
*
|
||||
* A partir da arquitetura nova existe apenas
|
||||
* UM ID canônico para a rua.
|
||||
*/
|
||||
string id =
|
||||
rua.Id;
|
||||
|
||||
|
||||
mapa.features.Add(
|
||||
new MapaFeatureModel
|
||||
{
|
||||
type =
|
||||
"Feature",
|
||||
|
||||
properties =
|
||||
new MapaFeaturePropertiesModel
|
||||
{
|
||||
Id =
|
||||
id,
|
||||
|
||||
Name =
|
||||
string.IsNullOrWhiteSpace(
|
||||
rua.Nome
|
||||
)
|
||||
? $"Rua {id}"
|
||||
: rua.Nome,
|
||||
|
||||
Length =
|
||||
rua.ComprimentoMetros,
|
||||
|
||||
Dist1 =
|
||||
rua.Dist1,
|
||||
|
||||
Dist2 =
|
||||
rua.Dist2
|
||||
},
|
||||
|
||||
geometry =
|
||||
new MapaFeatureGeometryModel
|
||||
{
|
||||
id =
|
||||
id,
|
||||
|
||||
type =
|
||||
"LineString",
|
||||
|
||||
coordinates =
|
||||
coordenadas
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return mapa;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,527 @@
|
|||
using OperationControl.Mapas.Models;
|
||||
|
||||
namespace OperationControl.Mapas.Services
|
||||
{
|
||||
public sealed class MapaMovimentoService
|
||||
{
|
||||
private const double
|
||||
RaioTerraMetros =
|
||||
6371000.0;
|
||||
|
||||
|
||||
public MapaMovimentoRover ObterOuCriar(
|
||||
MapaEstadoMovimento estado,
|
||||
string roverId)
|
||||
{
|
||||
ValidarEstado(
|
||||
estado
|
||||
);
|
||||
|
||||
|
||||
roverId =
|
||||
NormalizarId(
|
||||
roverId
|
||||
);
|
||||
|
||||
|
||||
if (
|
||||
estado.RoversMutaveis
|
||||
.TryGetValue(
|
||||
roverId,
|
||||
out var existente
|
||||
)
|
||||
)
|
||||
{
|
||||
return existente;
|
||||
}
|
||||
|
||||
|
||||
var movimento =
|
||||
new MapaMovimentoRover
|
||||
{
|
||||
RoverId =
|
||||
roverId
|
||||
};
|
||||
|
||||
|
||||
estado.RoversMutaveis[
|
||||
roverId
|
||||
] =
|
||||
movimento;
|
||||
|
||||
|
||||
return movimento;
|
||||
}
|
||||
|
||||
|
||||
public MapaMovimentoRover? Obter(
|
||||
MapaEstadoMovimento estado,
|
||||
string roverId)
|
||||
{
|
||||
ValidarEstado(
|
||||
estado
|
||||
);
|
||||
|
||||
|
||||
if (
|
||||
string.IsNullOrWhiteSpace(
|
||||
roverId
|
||||
)
|
||||
)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
estado.RoversMutaveis
|
||||
.TryGetValue(
|
||||
roverId.Trim(),
|
||||
out var movimento
|
||||
);
|
||||
|
||||
|
||||
return movimento;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Atualiza a posição histórica do rover.
|
||||
///
|
||||
/// Retorna true somente quando um novo
|
||||
/// ponto foi realmente incorporado ao rastro.
|
||||
/// </summary>
|
||||
public bool AtualizarPosicao(
|
||||
MapaEstadoMovimento estado,
|
||||
string roverId,
|
||||
double latitude,
|
||||
double longitude,
|
||||
bool pulverizando,
|
||||
double distanciaMinimaMetros = 0.05,
|
||||
double distanciaMaximaSegmentoMetros = 20.0)
|
||||
{
|
||||
ValidarCoordenada(
|
||||
latitude,
|
||||
longitude
|
||||
);
|
||||
|
||||
|
||||
if (
|
||||
!double.IsFinite(
|
||||
distanciaMinimaMetros
|
||||
) ||
|
||||
distanciaMinimaMetros < 0
|
||||
)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(
|
||||
distanciaMinimaMetros
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (
|
||||
!double.IsFinite(
|
||||
distanciaMaximaSegmentoMetros
|
||||
) ||
|
||||
distanciaMaximaSegmentoMetros <= 0
|
||||
)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(
|
||||
distanciaMaximaSegmentoMetros
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
var movimento =
|
||||
ObterOuCriar(
|
||||
estado,
|
||||
roverId
|
||||
);
|
||||
|
||||
|
||||
var atual =
|
||||
new MapaPontoWgs84(
|
||||
latitude,
|
||||
longitude
|
||||
);
|
||||
|
||||
|
||||
var anterior =
|
||||
movimento
|
||||
.UltimaPosicaoAceita;
|
||||
|
||||
|
||||
/*
|
||||
* Primeira posição:
|
||||
*
|
||||
* apenas estabelece a origem.
|
||||
* Não existe segmento ainda.
|
||||
*/
|
||||
if (anterior == null)
|
||||
{
|
||||
movimento.UltimaPosicaoAceita =
|
||||
atual;
|
||||
|
||||
movimento.QuebrarRastro =
|
||||
true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
double distancia =
|
||||
DistanciaMetros(
|
||||
anterior,
|
||||
atual
|
||||
);
|
||||
|
||||
|
||||
/*
|
||||
* Evita milhares de pontos praticamente
|
||||
* iguais por jitter ou alta frequência.
|
||||
*/
|
||||
if (
|
||||
distancia <
|
||||
distanciaMinimaMetros
|
||||
)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Salto impossível / reconexão / posição
|
||||
* antiga:
|
||||
*
|
||||
* reposiciona a origem mas NÃO desenha
|
||||
* uma linha atravessando o talhão.
|
||||
*/
|
||||
if (
|
||||
distancia >
|
||||
distanciaMaximaSegmentoMetros
|
||||
)
|
||||
{
|
||||
movimento.UltimaPosicaoAceita =
|
||||
atual;
|
||||
|
||||
movimento.QuebrarRastro =
|
||||
true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
MapaTrechoRastro?
|
||||
trechoAtual =
|
||||
movimento
|
||||
.TrechosMutaveis
|
||||
.LastOrDefault();
|
||||
|
||||
|
||||
bool precisaNovoTrecho =
|
||||
movimento.QuebrarRastro ||
|
||||
trechoAtual == null ||
|
||||
trechoAtual.Pulverizando !=
|
||||
pulverizando;
|
||||
|
||||
|
||||
if (precisaNovoTrecho)
|
||||
{
|
||||
trechoAtual =
|
||||
new MapaTrechoRastro(
|
||||
pulverizando
|
||||
);
|
||||
|
||||
|
||||
/*
|
||||
* Começamos pelo ponto anterior
|
||||
* para manter continuidade visual.
|
||||
*/
|
||||
trechoAtual
|
||||
.PontosMutaveis
|
||||
.Add(
|
||||
anterior
|
||||
);
|
||||
|
||||
|
||||
movimento
|
||||
.TrechosMutaveis
|
||||
.Add(
|
||||
trechoAtual
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
trechoAtual
|
||||
.PontosMutaveis
|
||||
.Add(
|
||||
atual
|
||||
);
|
||||
|
||||
|
||||
movimento.UltimaPosicaoAceita =
|
||||
atual;
|
||||
|
||||
movimento.QuebrarRastro =
|
||||
false;
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Substitui a previsão MPC atual.
|
||||
///
|
||||
/// Menos de dois pontos significa que
|
||||
/// não existe previsão desenhável.
|
||||
/// </summary>
|
||||
public void DefinirPredicao(
|
||||
MapaEstadoMovimento estado,
|
||||
string roverId,
|
||||
IEnumerable<MapaPontoWgs84>?
|
||||
pontos)
|
||||
{
|
||||
var movimento =
|
||||
ObterOuCriar(
|
||||
estado,
|
||||
roverId
|
||||
);
|
||||
|
||||
|
||||
var nova =
|
||||
new List<MapaPontoWgs84>();
|
||||
|
||||
|
||||
if (pontos != null)
|
||||
{
|
||||
foreach (
|
||||
var ponto
|
||||
in pontos)
|
||||
{
|
||||
if (ponto == null)
|
||||
continue;
|
||||
|
||||
|
||||
ValidarCoordenada(
|
||||
ponto.Latitude,
|
||||
ponto.Longitude
|
||||
);
|
||||
|
||||
|
||||
nova.Add(
|
||||
new MapaPontoWgs84(
|
||||
ponto.Latitude,
|
||||
ponto.Longitude
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
movimento
|
||||
.PredicaoMutavel
|
||||
.Clear();
|
||||
|
||||
|
||||
/*
|
||||
* Isso também resolve um problema do
|
||||
* legado: previsão velha não fica presa
|
||||
* quando o MPC para de enviar pontos.
|
||||
*/
|
||||
if (nova.Count >= 2)
|
||||
{
|
||||
movimento
|
||||
.PredicaoMutavel
|
||||
.AddRange(
|
||||
nova
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void LimparRastro(
|
||||
MapaEstadoMovimento estado,
|
||||
string roverId,
|
||||
bool limparPredicao = true)
|
||||
{
|
||||
var movimento =
|
||||
Obter(
|
||||
estado,
|
||||
roverId
|
||||
);
|
||||
|
||||
|
||||
if (movimento == null)
|
||||
return;
|
||||
|
||||
|
||||
movimento
|
||||
.TrechosMutaveis
|
||||
.Clear();
|
||||
|
||||
|
||||
/*
|
||||
* Mantemos UltimaPosicaoAceita.
|
||||
*
|
||||
* Assim o próximo ponto parte da posição
|
||||
* atual, como o comportamento antigo.
|
||||
*/
|
||||
movimento.QuebrarRastro =
|
||||
true;
|
||||
|
||||
|
||||
if (limparPredicao)
|
||||
{
|
||||
movimento
|
||||
.PredicaoMutavel
|
||||
.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void Remover(
|
||||
MapaEstadoMovimento estado,
|
||||
string roverId)
|
||||
{
|
||||
ValidarEstado(
|
||||
estado
|
||||
);
|
||||
|
||||
|
||||
if (
|
||||
string.IsNullOrWhiteSpace(
|
||||
roverId
|
||||
)
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
estado.RoversMutaveis
|
||||
.Remove(
|
||||
roverId.Trim()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public void Limpar(
|
||||
MapaEstadoMovimento estado)
|
||||
{
|
||||
ValidarEstado(
|
||||
estado
|
||||
);
|
||||
|
||||
|
||||
estado.RoversMutaveis
|
||||
.Clear();
|
||||
}
|
||||
|
||||
|
||||
private static double DistanciaMetros(
|
||||
MapaPontoWgs84 a,
|
||||
MapaPontoWgs84 b)
|
||||
{
|
||||
double lat1 =
|
||||
a.Latitude *
|
||||
Math.PI / 180.0;
|
||||
|
||||
double lat2 =
|
||||
b.Latitude *
|
||||
Math.PI / 180.0;
|
||||
|
||||
double dLat =
|
||||
(
|
||||
b.Latitude -
|
||||
a.Latitude
|
||||
) *
|
||||
Math.PI / 180.0;
|
||||
|
||||
double dLon =
|
||||
(
|
||||
b.Longitude -
|
||||
a.Longitude
|
||||
) *
|
||||
Math.PI / 180.0;
|
||||
|
||||
|
||||
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);
|
||||
|
||||
|
||||
double c =
|
||||
2.0 *
|
||||
Math.Atan2(
|
||||
Math.Sqrt(h),
|
||||
Math.Sqrt(1.0 - h)
|
||||
);
|
||||
|
||||
|
||||
return
|
||||
RaioTerraMetros *
|
||||
c;
|
||||
}
|
||||
|
||||
|
||||
private static void ValidarCoordenada(
|
||||
double latitude,
|
||||
double longitude)
|
||||
{
|
||||
if (
|
||||
!double.IsFinite(latitude) ||
|
||||
!double.IsFinite(longitude) ||
|
||||
latitude < -90.0 ||
|
||||
latitude > 90.0 ||
|
||||
longitude < -180.0 ||
|
||||
longitude > 180.0
|
||||
)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(latitude),
|
||||
"Coordenada WGS84 inválida."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static string NormalizarId(
|
||||
string roverId)
|
||||
{
|
||||
if (
|
||||
string.IsNullOrWhiteSpace(
|
||||
roverId
|
||||
)
|
||||
)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"RoverId inválido.",
|
||||
nameof(roverId)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return roverId.Trim();
|
||||
}
|
||||
|
||||
|
||||
private static void ValidarEstado(
|
||||
MapaEstadoMovimento estado)
|
||||
{
|
||||
if (estado == null)
|
||||
{
|
||||
throw new ArgumentNullException(
|
||||
nameof(estado)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,433 @@
|
|||
using OperationControl.Mapas.Models;
|
||||
|
||||
namespace OperationControl.Mapas.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Gerencia a rota manual de retorno.
|
||||
///
|
||||
/// Não conhece Mapsui.
|
||||
/// Não conhece WPF.
|
||||
/// Não conhece mouse.
|
||||
///
|
||||
/// Internamente trabalha sempre com propriedades
|
||||
/// explícitas Latitude/Longitude.
|
||||
/// </summary>
|
||||
public sealed class MapaRotaRetornoService
|
||||
{
|
||||
public const double
|
||||
ToleranciaRemocaoPadraoMetros =
|
||||
2.0;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Alterna um ponto:
|
||||
///
|
||||
/// - se não existir ponto próximo, adiciona ao final;
|
||||
/// - se existir ponto dentro da tolerância, remove o mais próximo.
|
||||
///
|
||||
/// Retorna true quando adicionou.
|
||||
/// Retorna false quando removeu.
|
||||
/// </summary>
|
||||
public bool AlternarPonto(
|
||||
MapaRotaRetorno rota,
|
||||
double latitude,
|
||||
double longitude,
|
||||
double toleranciaMetros =
|
||||
ToleranciaRemocaoPadraoMetros)
|
||||
{
|
||||
ValidarRota(
|
||||
rota
|
||||
);
|
||||
|
||||
ValidarCoordenada(
|
||||
latitude,
|
||||
longitude
|
||||
);
|
||||
|
||||
if (
|
||||
double.IsNaN(toleranciaMetros) ||
|
||||
double.IsInfinity(toleranciaMetros) ||
|
||||
toleranciaMetros < 0
|
||||
)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(toleranciaMetros),
|
||||
"A tolerância deve ser finita e não negativa."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
var novo =
|
||||
new MapaPontoRetorno(
|
||||
latitude,
|
||||
longitude
|
||||
);
|
||||
|
||||
|
||||
int indiceProximo =
|
||||
EncontrarIndiceMaisProximo(
|
||||
rota,
|
||||
novo,
|
||||
toleranciaMetros
|
||||
);
|
||||
|
||||
|
||||
if (indiceProximo >= 0)
|
||||
{
|
||||
rota.PontosMutaveis
|
||||
.RemoveAt(
|
||||
indiceProximo
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
rota.PontosMutaveis.Add(
|
||||
novo
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public void AdicionarPonto(
|
||||
MapaRotaRetorno rota,
|
||||
double latitude,
|
||||
double longitude)
|
||||
{
|
||||
ValidarRota(
|
||||
rota
|
||||
);
|
||||
|
||||
ValidarCoordenada(
|
||||
latitude,
|
||||
longitude
|
||||
);
|
||||
|
||||
|
||||
rota.PontosMutaveis.Add(
|
||||
new MapaPontoRetorno(
|
||||
latitude,
|
||||
longitude
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public void Limpar(
|
||||
MapaRotaRetorno rota)
|
||||
{
|
||||
ValidarRota(
|
||||
rota
|
||||
);
|
||||
|
||||
rota.PontosMutaveis.Clear();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Distância acumulada entre os pontos da rota.
|
||||
///
|
||||
/// Com zero ou um ponto, retorna 0.
|
||||
/// </summary>
|
||||
public double ObterDistanciaTotalMetros(
|
||||
MapaRotaRetorno rota)
|
||||
{
|
||||
ValidarRota(
|
||||
rota
|
||||
);
|
||||
|
||||
|
||||
if (rota.Quantidade < 2)
|
||||
return 0.0;
|
||||
|
||||
|
||||
double total =
|
||||
0.0;
|
||||
|
||||
|
||||
for (
|
||||
int i = 1;
|
||||
i < rota.Pontos.Count;
|
||||
i++)
|
||||
{
|
||||
total += DistanciaMetros(
|
||||
rota.Pontos[i - 1],
|
||||
rota.Pontos[i]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Converte para o contrato legado atual do rover.
|
||||
///
|
||||
/// ATENÇÃO:
|
||||
/// PontosRetorno trafega como:
|
||||
///
|
||||
/// [latitude, longitude]
|
||||
///
|
||||
/// Isso é diferente do GeoJSON:
|
||||
///
|
||||
/// [longitude, latitude]
|
||||
///
|
||||
/// Essa peculiaridade fica confinada SOMENTE aqui.
|
||||
/// </summary>
|
||||
public List<double[]> ExportarContrato(
|
||||
MapaRotaRetorno rota)
|
||||
{
|
||||
ValidarRota(
|
||||
rota
|
||||
);
|
||||
|
||||
|
||||
return rota.Pontos
|
||||
.Select(
|
||||
x =>
|
||||
new[]
|
||||
{
|
||||
x.Latitude,
|
||||
x.Longitude
|
||||
}
|
||||
)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Reconstrói a rota a partir do contrato recebido
|
||||
/// do rover.
|
||||
///
|
||||
/// O método é transacional:
|
||||
/// somente altera a rota depois que todos
|
||||
/// os pontos forem validados.
|
||||
/// </summary>
|
||||
public void DefinirPeloContrato(
|
||||
MapaRotaRetorno rota,
|
||||
IEnumerable<double[]>?
|
||||
pontosContrato)
|
||||
{
|
||||
ValidarRota(
|
||||
rota
|
||||
);
|
||||
|
||||
|
||||
var novosPontos =
|
||||
new List<MapaPontoRetorno>();
|
||||
|
||||
|
||||
foreach (
|
||||
double[] item
|
||||
in pontosContrato
|
||||
?? Enumerable.Empty<double[]>())
|
||||
{
|
||||
if (
|
||||
item == null ||
|
||||
item.Length < 2
|
||||
)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Foi recebido um ponto de retorno incompleto."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Contrato atual:
|
||||
*
|
||||
* [0] = latitude
|
||||
* [1] = longitude
|
||||
*/
|
||||
double latitude =
|
||||
item[0];
|
||||
|
||||
double longitude =
|
||||
item[1];
|
||||
|
||||
|
||||
ValidarCoordenada(
|
||||
latitude,
|
||||
longitude
|
||||
);
|
||||
|
||||
|
||||
novosPontos.Add(
|
||||
new MapaPontoRetorno(
|
||||
latitude,
|
||||
longitude
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
rota.PontosMutaveis.Clear();
|
||||
|
||||
rota.PontosMutaveis.AddRange(
|
||||
novosPontos
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private static int
|
||||
EncontrarIndiceMaisProximo(
|
||||
MapaRotaRetorno rota,
|
||||
MapaPontoRetorno referencia,
|
||||
double toleranciaMetros)
|
||||
{
|
||||
int melhorIndice =
|
||||
-1;
|
||||
|
||||
double melhorDistancia =
|
||||
double.PositiveInfinity;
|
||||
|
||||
|
||||
for (
|
||||
int i = 0;
|
||||
i < rota.Pontos.Count;
|
||||
i++)
|
||||
{
|
||||
double distancia =
|
||||
DistanciaMetros(
|
||||
referencia,
|
||||
rota.Pontos[i]
|
||||
);
|
||||
|
||||
|
||||
if (
|
||||
distancia <= toleranciaMetros &&
|
||||
distancia < melhorDistancia
|
||||
)
|
||||
{
|
||||
melhorIndice =
|
||||
i;
|
||||
|
||||
melhorDistancia =
|
||||
distancia;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return melhorIndice;
|
||||
}
|
||||
|
||||
|
||||
private static double DistanciaMetros(
|
||||
MapaPontoRetorno a,
|
||||
MapaPontoRetorno b)
|
||||
{
|
||||
const double raioTerra =
|
||||
6371000.0;
|
||||
|
||||
|
||||
double lat1 =
|
||||
GrausParaRadianos(
|
||||
a.Latitude
|
||||
);
|
||||
|
||||
double lat2 =
|
||||
GrausParaRadianos(
|
||||
b.Latitude
|
||||
);
|
||||
|
||||
double deltaLat =
|
||||
GrausParaRadianos(
|
||||
b.Latitude -
|
||||
a.Latitude
|
||||
);
|
||||
|
||||
double deltaLon =
|
||||
GrausParaRadianos(
|
||||
b.Longitude -
|
||||
a.Longitude
|
||||
);
|
||||
|
||||
|
||||
double h =
|
||||
Math.Sin(
|
||||
deltaLat / 2.0
|
||||
) *
|
||||
Math.Sin(
|
||||
deltaLat / 2.0
|
||||
) +
|
||||
Math.Cos(lat1) *
|
||||
Math.Cos(lat2) *
|
||||
Math.Sin(
|
||||
deltaLon / 2.0
|
||||
) *
|
||||
Math.Sin(
|
||||
deltaLon / 2.0
|
||||
);
|
||||
|
||||
|
||||
double c =
|
||||
2.0 *
|
||||
Math.Atan2(
|
||||
Math.Sqrt(h),
|
||||
Math.Sqrt(1.0 - h)
|
||||
);
|
||||
|
||||
|
||||
return raioTerra * c;
|
||||
}
|
||||
|
||||
|
||||
private static double
|
||||
GrausParaRadianos(
|
||||
double graus)
|
||||
{
|
||||
return graus *
|
||||
Math.PI /
|
||||
180.0;
|
||||
}
|
||||
|
||||
|
||||
private static void
|
||||
ValidarCoordenada(
|
||||
double latitude,
|
||||
double longitude)
|
||||
{
|
||||
if (
|
||||
double.IsNaN(latitude) ||
|
||||
double.IsInfinity(latitude) ||
|
||||
latitude < -90.0 ||
|
||||
latitude > 90.0
|
||||
)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(latitude),
|
||||
"Latitude WGS84 inválida."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (
|
||||
double.IsNaN(longitude) ||
|
||||
double.IsInfinity(longitude) ||
|
||||
longitude < -180.0 ||
|
||||
longitude > 180.0
|
||||
)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(longitude),
|
||||
"Longitude WGS84 inválida."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static void ValidarRota(
|
||||
MapaRotaRetorno rota)
|
||||
{
|
||||
if (rota == null)
|
||||
{
|
||||
throw new ArgumentNullException(
|
||||
nameof(rota)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,417 @@
|
|||
using OperationControl.Mapas.Models;
|
||||
|
||||
namespace OperationControl.Mapas.Services
|
||||
{
|
||||
public class MapaSelectionService
|
||||
{
|
||||
/// <summary>
|
||||
/// Alterna o estado de seleção de uma rua.
|
||||
///
|
||||
/// Retorna true se a rua ficou selecionada.
|
||||
/// Retorna false se a rua ficou desmarcada.
|
||||
/// </summary>
|
||||
public bool AlternarRua(
|
||||
MapaDocumento documento,
|
||||
MapaSelecaoOperacao selecao,
|
||||
string ruaId)
|
||||
{
|
||||
ValidarArgumentos(
|
||||
documento,
|
||||
selecao
|
||||
);
|
||||
|
||||
|
||||
string id =
|
||||
NormalizarId(
|
||||
ruaId
|
||||
);
|
||||
|
||||
|
||||
GarantirRuaExistente(
|
||||
documento,
|
||||
id
|
||||
);
|
||||
|
||||
|
||||
if (selecao.RuasMutaveis.Contains(
|
||||
id,
|
||||
StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
RemoverRua(
|
||||
documento,
|
||||
selecao,
|
||||
id
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
SelecionarRua(
|
||||
documento,
|
||||
selecao,
|
||||
id
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Seleciona uma rua e a coloca no final da ordem atual.
|
||||
///
|
||||
/// Se já estiver selecionada, nada é alterado.
|
||||
/// </summary>
|
||||
public bool SelecionarRua(
|
||||
MapaDocumento documento,
|
||||
MapaSelecaoOperacao selecao,
|
||||
string ruaId)
|
||||
{
|
||||
ValidarArgumentos(
|
||||
documento,
|
||||
selecao
|
||||
);
|
||||
|
||||
|
||||
string id =
|
||||
NormalizarId(
|
||||
ruaId
|
||||
);
|
||||
|
||||
|
||||
GarantirRuaExistente(
|
||||
documento,
|
||||
id
|
||||
);
|
||||
|
||||
|
||||
if (selecao.RuasMutaveis.Contains(
|
||||
id,
|
||||
StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
selecao.RuasMutaveis.Add(
|
||||
id
|
||||
);
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Remove uma rua da seleção.
|
||||
///
|
||||
/// Como a ordem é representada pela própria posição
|
||||
/// na lista, nenhuma renumeração manual é necessária.
|
||||
/// </summary>
|
||||
public bool RemoverRua(
|
||||
MapaDocumento documento,
|
||||
MapaSelecaoOperacao selecao,
|
||||
string ruaId)
|
||||
{
|
||||
ValidarArgumentos(
|
||||
documento,
|
||||
selecao
|
||||
);
|
||||
|
||||
|
||||
string id =
|
||||
NormalizarId(
|
||||
ruaId
|
||||
);
|
||||
|
||||
|
||||
int indice =
|
||||
selecao.RuasMutaveis.FindIndex(
|
||||
x => string.Equals(
|
||||
x,
|
||||
id,
|
||||
StringComparison.OrdinalIgnoreCase
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
if (indice < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
selecao.RuasMutaveis.RemoveAt(
|
||||
indice
|
||||
);
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Substitui a seleção atual por uma lista já ordenada.
|
||||
///
|
||||
/// Útil para restaurar uma operação previamente
|
||||
/// parametrizada ou recebida de outro componente.
|
||||
///
|
||||
/// IDs inválidos, vazios ou duplicados não são aceitos.
|
||||
/// </summary>
|
||||
public void DefinirSelecao(
|
||||
MapaDocumento documento,
|
||||
MapaSelecaoOperacao selecao,
|
||||
IEnumerable<string>? ruasOrdenadas)
|
||||
{
|
||||
ValidarArgumentos(
|
||||
documento,
|
||||
selecao
|
||||
);
|
||||
|
||||
|
||||
var novaSelecao =
|
||||
new List<string>();
|
||||
|
||||
|
||||
var idsUsados =
|
||||
new HashSet<string>(
|
||||
StringComparer.OrdinalIgnoreCase
|
||||
);
|
||||
|
||||
|
||||
foreach (
|
||||
string? ruaId
|
||||
in ruasOrdenadas ?? Enumerable.Empty<string>())
|
||||
{
|
||||
string id =
|
||||
NormalizarId(
|
||||
ruaId
|
||||
);
|
||||
|
||||
|
||||
GarantirRuaExistente(
|
||||
documento,
|
||||
id
|
||||
);
|
||||
|
||||
|
||||
if (!idsUsados.Add(id))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"A seleção contém o ID de rua '{id}' repetido."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
novaSelecao.Add(
|
||||
id
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Só altera o objeto oficial depois que a nova
|
||||
* seleção inteira foi validada.
|
||||
*
|
||||
* Assim a operação é transacional:
|
||||
* se houver um ID inválido no meio da lista,
|
||||
* a seleção anterior permanece intacta.
|
||||
*/
|
||||
selecao.RuasMutaveis.Clear();
|
||||
|
||||
selecao.RuasMutaveis.AddRange(
|
||||
novaSelecao
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public void Limpar(
|
||||
MapaSelecaoOperacao selecao)
|
||||
{
|
||||
if (selecao == null)
|
||||
{
|
||||
throw new ArgumentNullException(
|
||||
nameof(selecao)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
selecao.RuasMutaveis.Clear();
|
||||
}
|
||||
|
||||
|
||||
public bool EstaSelecionada(
|
||||
MapaSelecaoOperacao selecao,
|
||||
string ruaId)
|
||||
{
|
||||
if (selecao == null)
|
||||
{
|
||||
throw new ArgumentNullException(
|
||||
nameof(selecao)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
string id =
|
||||
NormalizarId(
|
||||
ruaId
|
||||
);
|
||||
|
||||
|
||||
return selecao.RuasMutaveis.Contains(
|
||||
id,
|
||||
StringComparer.OrdinalIgnoreCase
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Retorna a ordem 1-based da rua.
|
||||
///
|
||||
/// Exemplo:
|
||||
/// primeira selecionada = 1.
|
||||
///
|
||||
/// Retorna null caso a rua não esteja selecionada.
|
||||
/// </summary>
|
||||
public int? ObterOrdem(
|
||||
MapaSelecaoOperacao selecao,
|
||||
string ruaId)
|
||||
{
|
||||
if (selecao == null)
|
||||
{
|
||||
throw new ArgumentNullException(
|
||||
nameof(selecao)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
string id =
|
||||
NormalizarId(
|
||||
ruaId
|
||||
);
|
||||
|
||||
|
||||
int indice =
|
||||
selecao.RuasMutaveis.FindIndex(
|
||||
x => string.Equals(
|
||||
x,
|
||||
id,
|
||||
StringComparison.OrdinalIgnoreCase
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
return indice < 0
|
||||
? null
|
||||
: indice + 1;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Retorna uma cópia independente da seleção.
|
||||
///
|
||||
/// Este é o método que futuramente alimentará
|
||||
/// OperacaoParametrosModel.RuasPercorrer.
|
||||
/// </summary>
|
||||
public List<string> ObterRuasOrdenadas(
|
||||
MapaSelecaoOperacao selecao)
|
||||
{
|
||||
if (selecao == null)
|
||||
{
|
||||
throw new ArgumentNullException(
|
||||
nameof(selecao)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return selecao.RuasOrdenadas
|
||||
.ToList();
|
||||
}
|
||||
|
||||
|
||||
private static void GarantirRuaExistente(
|
||||
MapaDocumento documento,
|
||||
string ruaId)
|
||||
{
|
||||
int quantidade =
|
||||
documento.Ruas.Count(
|
||||
x =>
|
||||
x != null &&
|
||||
string.Equals(
|
||||
x.Id,
|
||||
ruaId,
|
||||
StringComparison.OrdinalIgnoreCase
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
if (quantidade == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"A rua '{ruaId}' não existe no mapa."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Isso jamais deveria acontecer depois que o
|
||||
* MapaValidationService aprovou o documento.
|
||||
*
|
||||
* Mesmo assim protegemos esta fronteira.
|
||||
*/
|
||||
if (quantidade > 1)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"O mapa possui {quantidade} ruas com o ID '{ruaId}'."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static string NormalizarId(
|
||||
string? ruaId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(
|
||||
ruaId))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"O ID da rua não foi informado.",
|
||||
nameof(ruaId)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return ruaId.Trim();
|
||||
}
|
||||
|
||||
|
||||
private static void ValidarArgumentos(
|
||||
MapaDocumento documento,
|
||||
MapaSelecaoOperacao selecao)
|
||||
{
|
||||
if (documento == null)
|
||||
{
|
||||
throw new ArgumentNullException(
|
||||
nameof(documento)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (selecao == null)
|
||||
{
|
||||
throw new ArgumentNullException(
|
||||
nameof(selecao)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (documento.Ruas == null ||
|
||||
documento.Ruas.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"O mapa não possui ruas para seleção."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,273 @@
|
|||
using NetTopologySuite.Geometries;
|
||||
using OperationControl.Mapas.Models;
|
||||
|
||||
namespace OperationControl.Mapas.Services
|
||||
{
|
||||
public class MapaValidationService
|
||||
{
|
||||
public MapaResultadoValidacao Validar(
|
||||
MapaDocumento? documento)
|
||||
{
|
||||
var resultado =
|
||||
new MapaResultadoValidacao();
|
||||
|
||||
|
||||
if (documento == null)
|
||||
{
|
||||
resultado.AdicionarErro(
|
||||
"O documento de mapa é nulo."
|
||||
);
|
||||
|
||||
return resultado;
|
||||
}
|
||||
|
||||
|
||||
if (documento.Ruas == null ||
|
||||
documento.Ruas.Count == 0)
|
||||
{
|
||||
resultado.AdicionarErro(
|
||||
"O mapa não possui ruas."
|
||||
);
|
||||
|
||||
return resultado;
|
||||
}
|
||||
|
||||
|
||||
var idsUsados =
|
||||
new HashSet<string>(
|
||||
StringComparer.OrdinalIgnoreCase
|
||||
);
|
||||
|
||||
|
||||
for (
|
||||
int i = 0;
|
||||
i < documento.Ruas.Count;
|
||||
i++)
|
||||
{
|
||||
MapaRua? rua =
|
||||
documento.Ruas[i];
|
||||
|
||||
int numeroRua =
|
||||
i + 1;
|
||||
|
||||
|
||||
if (rua == null)
|
||||
{
|
||||
resultado.AdicionarErro(
|
||||
$"A rua na posição {numeroRua} é nula."
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
ValidarId(
|
||||
rua,
|
||||
numeroRua,
|
||||
idsUsados,
|
||||
resultado
|
||||
);
|
||||
|
||||
|
||||
ValidarGeometria(
|
||||
rua,
|
||||
numeroRua,
|
||||
resultado
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return resultado;
|
||||
}
|
||||
|
||||
|
||||
private static void ValidarId(
|
||||
MapaRua rua,
|
||||
int indice,
|
||||
HashSet<string> idsUsados,
|
||||
MapaResultadoValidacao resultado)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rua.Id))
|
||||
{
|
||||
resultado.AdicionarErro(
|
||||
$"A rua na posição {indice} não possui ID."
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
string id =
|
||||
rua.Id.Trim();
|
||||
|
||||
|
||||
if (!idsUsados.Add(id))
|
||||
{
|
||||
resultado.AdicionarErro(
|
||||
$"O ID de rua '{id}' está duplicado."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static void ValidarGeometria(
|
||||
MapaRua rua,
|
||||
int indice,
|
||||
MapaResultadoValidacao resultado)
|
||||
{
|
||||
LineString? geometria =
|
||||
rua.Geometria;
|
||||
|
||||
|
||||
if (geometria == null)
|
||||
{
|
||||
resultado.AdicionarErro(
|
||||
$"A rua '{ObterNomeRua(rua, indice)}' " +
|
||||
"não possui geometria."
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (geometria.IsEmpty)
|
||||
{
|
||||
resultado.AdicionarErro(
|
||||
$"A rua '{ObterNomeRua(rua, indice)}' " +
|
||||
"possui geometria vazia."
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (geometria.NumPoints < 2)
|
||||
{
|
||||
resultado.AdicionarErro(
|
||||
$"A rua '{ObterNomeRua(rua, indice)}' " +
|
||||
"possui menos de dois pontos."
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Coordinate[] coordenadas =
|
||||
geometria.Coordinates;
|
||||
|
||||
|
||||
bool possuiSegmentoReal = false;
|
||||
|
||||
|
||||
for (
|
||||
int i = 0;
|
||||
i < coordenadas.Length;
|
||||
i++)
|
||||
{
|
||||
Coordinate coordenada =
|
||||
coordenadas[i];
|
||||
|
||||
|
||||
double longitude =
|
||||
coordenada.X;
|
||||
|
||||
double latitude =
|
||||
coordenada.Y;
|
||||
|
||||
|
||||
bool valida =
|
||||
double.IsFinite(longitude) &&
|
||||
double.IsFinite(latitude) &&
|
||||
longitude >= -180.0 &&
|
||||
longitude <= 180.0 &&
|
||||
latitude >= -90.0 &&
|
||||
latitude <= 90.0;
|
||||
|
||||
|
||||
if (!valida)
|
||||
{
|
||||
resultado.AdicionarErro(
|
||||
$"A rua '{ObterNomeRua(rua, indice)}', " +
|
||||
$"ponto {i + 1}, possui coordenada inválida: " +
|
||||
$"longitude={longitude}, latitude={latitude}."
|
||||
);
|
||||
|
||||
/*
|
||||
* Não precisamos continuar analisando
|
||||
* a topologia dessa coordenada.
|
||||
*/
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
if (i > 0)
|
||||
{
|
||||
Coordinate anterior =
|
||||
coordenadas[i - 1];
|
||||
|
||||
|
||||
if (
|
||||
Math.Abs(
|
||||
anterior.X -
|
||||
coordenada.X
|
||||
) > 1e-12 ||
|
||||
Math.Abs(
|
||||
anterior.Y -
|
||||
coordenada.Y
|
||||
) > 1e-12
|
||||
)
|
||||
{
|
||||
possuiSegmentoReal =
|
||||
true;
|
||||
}
|
||||
else
|
||||
{
|
||||
resultado.AdicionarAviso(
|
||||
$"A rua '{ObterNomeRua(rua, indice)}' " +
|
||||
$"possui pontos consecutivos repetidos " +
|
||||
$"nas posições {i} e {i + 1}."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!possuiSegmentoReal)
|
||||
{
|
||||
resultado.AdicionarErro(
|
||||
$"A rua '{ObterNomeRua(rua, indice)}' " +
|
||||
"não possui deslocamento geométrico."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (
|
||||
!double.IsFinite(
|
||||
rua.ComprimentoMetros
|
||||
) ||
|
||||
rua.ComprimentoMetros < 0
|
||||
)
|
||||
{
|
||||
resultado.AdicionarErro(
|
||||
$"A rua '{ObterNomeRua(rua, indice)}' " +
|
||||
"possui comprimento inválido."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static string ObterNomeRua(
|
||||
MapaRua rua,
|
||||
int indice)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(
|
||||
rua.Id))
|
||||
{
|
||||
return rua.Id;
|
||||
}
|
||||
|
||||
|
||||
return indice.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ using System.Diagnostics;
|
|||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using static AgroBase.Models.Enums;
|
||||
using Application = System.Windows.Application;
|
||||
|
||||
namespace OperationControl.Models
|
||||
|
|
@ -1122,16 +1123,15 @@ namespace OperationControl.Models
|
|||
"Perda de comunicação com o equipamento!"
|
||||
);
|
||||
|
||||
Variaveis.Dock?
|
||||
._vm?
|
||||
._viewOperacaoCenter?
|
||||
.Mapa?
|
||||
.markers?
|
||||
.UpdateMarkerInfo(
|
||||
rover.RoverId,
|
||||
status:
|
||||
AgroBase.Models.Enums
|
||||
.StatusOperacao.Erro
|
||||
Application.Current?
|
||||
.Dispatcher?
|
||||
.BeginInvoke(
|
||||
new Action(
|
||||
() =>
|
||||
{
|
||||
Variaveis.Dock?._vm?.AtualizarStatusRoverMapa(rover.RoverId, StatusOperacao.Erro);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
else
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@
|
|||
<PackageReference Include="NetTopologySuite.IO.GeoJSON" Version="4.0.0" />
|
||||
<PackageReference Include="NetTopologySuite.IO.ShapeFile" Version="2.1.0" />
|
||||
<PackageReference Include="OpenTK" Version="4.9.4" />
|
||||
<PackageReference Include="ProjNET" Version="2.1.0" />
|
||||
<PackageReference Include="ScottPlot.WPF" Version="5.1.57" />
|
||||
<PackageReference Include="System.IO.Ports" Version="10.0.0" />
|
||||
<PackageReference Include="VideoLAN.LibVLC.Windows" Version="3.0.21" />
|
||||
|
|
|
|||
|
|
@ -43,6 +43,9 @@
|
|||
<Compile Update="Controls\TemperatureIndicator.xaml.cs">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Update="Mapas\Editing\Views\MapaEditorOverlay.xaml.cs">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Update="Views\ConfigConexao\ConfigConexaoView.xaml.cs">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
|
|
@ -85,6 +88,9 @@
|
|||
<Compile Update="Views\PreparacaoMapa\PreparacaoMapaBottomView.xaml.cs">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Update="Views\PreparacaoMapa\PreparacaoMapaStatusView.xaml.cs">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Update="Views\PreparacaoMapa\PreparacaoMapaTopView.xaml.cs">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
|
|
|
|||
|
|
@ -589,15 +589,41 @@ namespace OperationControl.Services
|
|||
|
||||
if (!fixar)
|
||||
{
|
||||
Models.Variaveis.MostrarLog("Configurando explicitamente o módulo como rover parado.");
|
||||
_expectedRole = GnssExpectedRole.RoverTemporary;
|
||||
await BaseFix.ConfigurarComoRoverParadoAsync(
|
||||
portaUsb,
|
||||
portaEntrada,
|
||||
_lifetimeCts.Token).ConfigureAwait(false);
|
||||
Models.Variaveis.MostrarLog(
|
||||
"Configurando explicitamente o módulo como rover parado."
|
||||
);
|
||||
|
||||
|
||||
await BaseFix
|
||||
.ConfigurarComoRoverParadoAsync(
|
||||
portaUsb,
|
||||
portaEntrada,
|
||||
_lifetimeCts.Token
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
|
||||
DefinirPapelEsperado(
|
||||
GnssExpectedRole.RoverTemporary
|
||||
);
|
||||
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* Vamos iniciar uma configuração de BASE.
|
||||
*
|
||||
* Se estávamos fazendo levantamento móvel
|
||||
* com NTRIP, encerramos a injeção RTCM antes
|
||||
* de começar a enviar os comandos da base.
|
||||
*
|
||||
* No método NTRIP, FixarBaseViaNtripAsync()
|
||||
* iniciará novamente a correção no momento
|
||||
* correto.
|
||||
*/
|
||||
await StopNtripAsync().ConfigureAwait(false);
|
||||
|
||||
bool sucesso = false;
|
||||
string mensagem;
|
||||
|
||||
|
|
@ -697,7 +723,9 @@ namespace OperationControl.Services
|
|||
if (sucesso)
|
||||
{
|
||||
var snapshot = GetPositionSnapshot();
|
||||
_expectedRole = GnssExpectedRole.BaseFixed;
|
||||
DefinirPapelEsperado(
|
||||
GnssExpectedRole.BaseFixed
|
||||
);
|
||||
_lastBaseConfiguration = new BaseFixedConfiguration
|
||||
{
|
||||
PortaUsb = portaUsb,
|
||||
|
|
@ -771,7 +799,86 @@ namespace OperationControl.Services
|
|||
|
||||
await Task.Delay(1000, ct).ConfigureAwait(false);
|
||||
await SendCommandsAsync(commands, 300, ct).ConfigureAwait(false);
|
||||
_expectedRole = GnssExpectedRole.BaseFixed;
|
||||
|
||||
if (AppShell.Mock)
|
||||
_expectedRole = GnssExpectedRole.BaseFixed;
|
||||
}
|
||||
|
||||
public async Task ConfigurarComoMovelParaLevantamentoAsync(bool usarNtrip = true)
|
||||
{
|
||||
if (!IsConnected)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"O GNSS não está conectado."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (
|
||||
BaseFix?.CorrecaoEmAndamento ==
|
||||
true
|
||||
)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Existe uma fixação da base em andamento."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Models.Variaveis.MostrarLog(
|
||||
"[GNSS][PAPEL] " +
|
||||
"Alterando para modo móvel / levantamento..."
|
||||
);
|
||||
|
||||
|
||||
/*
|
||||
* Se existia uma sessão NTRIP anterior,
|
||||
* encerramos antes de enviar comandos ASCII.
|
||||
*
|
||||
* Isso evita RTCM binário concorrendo com
|
||||
* configuração do UM982.
|
||||
*/
|
||||
await StopNtripAsync()
|
||||
.ConfigureAwait(false);
|
||||
|
||||
|
||||
BaseFix?.ReiniciarFix();
|
||||
|
||||
|
||||
/*
|
||||
* ConfigurarModulo(false):
|
||||
*
|
||||
* - unlog
|
||||
* - mode rover uav
|
||||
* - GGA
|
||||
* - heading
|
||||
* - papel esperado = RoverTemporary
|
||||
*/
|
||||
await ConfigurarModulo(
|
||||
fixar: false
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
|
||||
/*
|
||||
* Para levantamento queremos precisão RTK.
|
||||
*
|
||||
* Portanto o receptor volta a receber
|
||||
* correções do caster depois de estar em
|
||||
* modo rover.
|
||||
*/
|
||||
if (usarNtrip)
|
||||
{
|
||||
await StartNtripAsync()
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
|
||||
Models.Variaveis.MostrarLog(
|
||||
"[GNSS][PAPEL] " +
|
||||
$"Modo móvel ativo | " +
|
||||
$"NTRIP={(usarNtrip ? "SOLICITADO" : "NÃO")}"
|
||||
);
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
|
|
@ -2148,6 +2255,53 @@ namespace OperationControl.Services
|
|||
BaseFixed,
|
||||
}
|
||||
|
||||
|
||||
public enum GnssPapelEstacao
|
||||
{
|
||||
Indefinido = 0,
|
||||
|
||||
Movel = 1,
|
||||
|
||||
BaseFixa = 2
|
||||
}
|
||||
|
||||
public GnssPapelEstacao PapelEstacaoAtual
|
||||
{
|
||||
get
|
||||
{
|
||||
return _expectedRole switch
|
||||
{
|
||||
GnssExpectedRole.RoverTemporary =>
|
||||
GnssPapelEstacao.Movel,
|
||||
|
||||
GnssExpectedRole.BaseFixed =>
|
||||
GnssPapelEstacao.BaseFixa,
|
||||
|
||||
/*
|
||||
* Quando acabamos de abrir o programa,
|
||||
* ainda podemos estar em Preserve.
|
||||
*
|
||||
* Se o próprio UM982 estiver reportando
|
||||
* BASE FIX, conseguimos reconhecer que
|
||||
* ele está funcionando como base.
|
||||
*/
|
||||
_ when
|
||||
GetFixQualitySnapshot() ==
|
||||
TiposCorrecaoGPS.BaseFix =>
|
||||
GnssPapelEstacao.BaseFixa,
|
||||
|
||||
_ =>
|
||||
GnssPapelEstacao.Indefinido
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private void DefinirPapelEsperado(GnssExpectedRole papel)
|
||||
{
|
||||
_expectedRole = papel;
|
||||
}
|
||||
|
||||
|
||||
private sealed class RtcmEnvelope
|
||||
{
|
||||
public int Type { get; init; }
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ namespace OperationControl.ViewModels
|
|||
private bool _redeOk;
|
||||
public bool RedeOk
|
||||
{
|
||||
get => _redeOk;
|
||||
get => _redeOk || AppShell.Mock;
|
||||
set
|
||||
{
|
||||
if (_redeOk != value)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,16 @@ namespace OperationControl.ViewModels.Views.Operacao.Monitoramento
|
|||
if (_parametros != value)
|
||||
{
|
||||
_parametros = value;
|
||||
OnPropertyChanged(nameof(Parametros));
|
||||
|
||||
AtualizarModoOperacaoAtual(
|
||||
_parametros?.Modo
|
||||
?? AgroBase.Models.Enums
|
||||
.ModoOperacao.NaoDefinido
|
||||
);
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(Parametros)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -37,34 +46,383 @@ namespace OperationControl.ViewModels.Views.Operacao.Monitoramento
|
|||
|
||||
private AgroBase.Models.Enums.ModoOperacao? modoOperacaoAtual { get; set; }
|
||||
|
||||
private int _quantidadeRuasMapa;
|
||||
|
||||
public int QuantidadeRuasMapa =>
|
||||
_quantidadeRuasMapa;
|
||||
|
||||
|
||||
private int _quantidadeRuasPercurso;
|
||||
|
||||
public int QuantidadeRuasPercurso =>
|
||||
_quantidadeRuasPercurso;
|
||||
|
||||
|
||||
private int _quantidadeRuasSelecionadasEdicao;
|
||||
|
||||
public int QuantidadeRuasSelecionadasEdicao =>
|
||||
_quantidadeRuasSelecionadasEdicao;
|
||||
|
||||
|
||||
private int _quantidadeCorredores;
|
||||
|
||||
public int QuantidadeCorredores
|
||||
{
|
||||
get => _quantidadeCorredores;
|
||||
|
||||
private set
|
||||
{
|
||||
if (
|
||||
_quantidadeCorredores ==
|
||||
value
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_quantidadeCorredores =
|
||||
value;
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(
|
||||
QuantidadeCorredores
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private double _distanciaOperacaoMetros;
|
||||
|
||||
public double DistanciaOperacaoMetros
|
||||
{
|
||||
get => _distanciaOperacaoMetros;
|
||||
|
||||
private set
|
||||
{
|
||||
if (
|
||||
Math.Abs(
|
||||
_distanciaOperacaoMetros -
|
||||
value
|
||||
) <
|
||||
0.001
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_distanciaOperacaoMetros =
|
||||
value;
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(
|
||||
DistanciaOperacaoMetros
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private double _areaOperacaoHectares;
|
||||
|
||||
public double AreaOperacaoHectares
|
||||
{
|
||||
get => _areaOperacaoHectares;
|
||||
|
||||
private set
|
||||
{
|
||||
if (
|
||||
Math.Abs(
|
||||
_areaOperacaoHectares -
|
||||
value
|
||||
) <
|
||||
0.000001
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_areaOperacaoHectares =
|
||||
value;
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(
|
||||
AreaOperacaoHectares
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private double _larguraMediaCorredorMetros;
|
||||
|
||||
public double LarguraMediaCorredorMetros
|
||||
{
|
||||
get => _larguraMediaCorredorMetros;
|
||||
|
||||
private set
|
||||
{
|
||||
if (
|
||||
Math.Abs(
|
||||
_larguraMediaCorredorMetros -
|
||||
value
|
||||
) <
|
||||
0.001
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_larguraMediaCorredorMetros =
|
||||
value;
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(
|
||||
LarguraMediaCorredorMetros
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private double? _tempoEstimadoOperacaoSegundos;
|
||||
|
||||
public double? TempoEstimadoOperacaoSegundos
|
||||
{
|
||||
get => _tempoEstimadoOperacaoSegundos;
|
||||
|
||||
private set
|
||||
{
|
||||
_tempoEstimadoOperacaoSegundos =
|
||||
value;
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(
|
||||
TempoEstimadoOperacaoSegundos
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private bool _edicaoMapaAtiva;
|
||||
|
||||
public bool EdicaoMapaAtiva =>
|
||||
_edicaoMapaAtiva;
|
||||
|
||||
|
||||
private bool _edicaoMapaModificada;
|
||||
|
||||
public bool EdicaoMapaModificada =>
|
||||
_edicaoMapaModificada;
|
||||
|
||||
|
||||
public bool PossuiMapaOperacional =>
|
||||
_quantidadeRuasMapa > 0;
|
||||
|
||||
|
||||
public bool PodeEditarMapa =>
|
||||
PossuiMapaOperacional &&
|
||||
!EdicaoMapaAtiva;
|
||||
|
||||
|
||||
public string MapaResumoTexto
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!PossuiMapaOperacional)
|
||||
{
|
||||
return
|
||||
"Nenhum mapa operacional carregado.";
|
||||
}
|
||||
|
||||
|
||||
return
|
||||
$"{QuantidadeRuasMapa} ruas no mapa | " +
|
||||
$"{QuantidadeRuasPercurso} no percurso";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public string EstadoEdicaoMapaTexto
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!PossuiMapaOperacional)
|
||||
{
|
||||
return
|
||||
"Carregue ou leia um mapa para editar.";
|
||||
}
|
||||
|
||||
|
||||
if (EdicaoMapaAtiva)
|
||||
{
|
||||
return EdicaoMapaModificada
|
||||
? "Edição em andamento • alterações pendentes"
|
||||
: "Edição em andamento";
|
||||
}
|
||||
|
||||
|
||||
return
|
||||
"Pronto para edição.";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public string SelecaoEdicaoMapaTexto
|
||||
{
|
||||
get
|
||||
{
|
||||
if (
|
||||
QuantidadeRuasSelecionadasEdicao ==
|
||||
1
|
||||
)
|
||||
{
|
||||
return
|
||||
"1 rua selecionada para edição";
|
||||
}
|
||||
|
||||
|
||||
return
|
||||
$"{QuantidadeRuasSelecionadasEdicao} " +
|
||||
"ruas selecionadas para edição";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public bool PossuiEstimativaMapa => QuantidadeCorredores > 0 && DistanciaOperacaoMetros > 0;
|
||||
|
||||
|
||||
public string MapaCorredoresTexto =>
|
||||
QuantidadeCorredores switch
|
||||
{
|
||||
0 =>
|
||||
"Selecione ao menos 2 ruas para formar um corredor.",
|
||||
|
||||
1 =>
|
||||
$"1 corredor • largura média " +
|
||||
$"{LarguraMediaCorredorMetros:0.00} m",
|
||||
|
||||
_ =>
|
||||
$"{QuantidadeCorredores} corredores • largura média " +
|
||||
$"{LarguraMediaCorredorMetros:0.00} m"
|
||||
};
|
||||
|
||||
|
||||
public string MapaDistanciaTexto => $"{DistanciaOperacaoMetros:N0} m";
|
||||
|
||||
|
||||
public string MapaAreaTexto => $"{AreaOperacaoHectares:0.000} ha";
|
||||
|
||||
|
||||
public string MapaTempoEstimadoTexto
|
||||
{
|
||||
get
|
||||
{
|
||||
if (
|
||||
!TempoEstimadoOperacaoSegundos
|
||||
.HasValue
|
||||
)
|
||||
{
|
||||
return "--:--:--";
|
||||
}
|
||||
|
||||
|
||||
double segundos =
|
||||
Math.Max(
|
||||
0,
|
||||
TempoEstimadoOperacaoSegundos
|
||||
.Value
|
||||
);
|
||||
|
||||
|
||||
var duracao =
|
||||
TimeSpan
|
||||
.FromSeconds(
|
||||
segundos
|
||||
);
|
||||
|
||||
|
||||
long horas =
|
||||
(long)Math.Floor(
|
||||
duracao.TotalHours
|
||||
);
|
||||
|
||||
|
||||
return string.Format(
|
||||
"{0:00}:{1:00}:{2:00}",
|
||||
horas,
|
||||
duracao.Minutes,
|
||||
duracao.Seconds
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public ParametrizacaoViewModel()
|
||||
{
|
||||
CarregarParametrosPadraoCommand = new RelayCommand(p => CarregarParametrosPadrao((AgroBase.Models.Enums.ModoOperacao)p));
|
||||
LerParametrosCommand = new RelayCommand(_ => LerParametrosSolicitado?.Invoke());
|
||||
SalvarParametrosCommand = new RelayCommand(_ => SalvarParametrosSolicitado?.Invoke(Parametros));
|
||||
LerParametrosCommand = new RelayCommand(_ => LerParametrosSolicitado?.Invoke(), _ => !EdicaoMapaAtiva);
|
||||
SalvarParametrosCommand =new RelayCommand(_ => SalvarParametrosSolicitado?.Invoke(Parametros), _ => !EdicaoMapaAtiva);
|
||||
EditarMapaCommand = new RelayCommand(_ => EditarMapaSolicitado?.Invoke(), _ => PodeEditarMapa);
|
||||
CancelarEdicaoMapaCommand = new RelayCommand(_ => CancelarEdicaoMapaSolicitado?.Invoke(), _ => EdicaoMapaAtiva);
|
||||
ReiniciarOperacaoCommand = new RelayCommand(_ => ReiniciarOperacao());
|
||||
}
|
||||
|
||||
|
||||
public event Action? LerParametrosSolicitado;
|
||||
public event Action<OperacaoParametrosModel>? SalvarParametrosSolicitado;
|
||||
public event Action? EditarMapaSolicitado;
|
||||
public event Action? CancelarEdicaoMapaSolicitado;
|
||||
|
||||
/// <summary>
|
||||
/// Disparado quando o modo desejado da parametrização muda.
|
||||
///
|
||||
/// O Dock usa esse evento para recalcular o modo de
|
||||
/// interação do mapa sem o MapViewControl conhecer
|
||||
/// ParametrizacaoViewModel ou estado global da aplicação.
|
||||
/// </summary>
|
||||
public event Action<AgroBase.Models.Enums.ModoOperacao>? ModoOperacaoAlterado;
|
||||
public ICommand LerParametrosCommand { get; }
|
||||
public ICommand SalvarParametrosCommand { get; }
|
||||
public ICommand CarregarParametrosPadraoCommand { get; }
|
||||
public ICommand ReiniciarOperacaoCommand { get; }
|
||||
public ICommand EditarMapaCommand { get; }
|
||||
public ICommand CancelarEdicaoMapaCommand { get; }
|
||||
public Array ModosOperacaoDisponiveis { get; } = Enum.GetValues(typeof(AgroBase.Models.Enums.ModoOperacao));
|
||||
|
||||
|
||||
private void CarregarParametrosPadrao(AgroBase.Models.Enums.ModoOperacao modo)
|
||||
{
|
||||
var new_op = OperacaoModel.CarregarParametrosOperacaoPadrao(modo, false);
|
||||
modoOperacaoAtual = new_op.Parametros.Modo;
|
||||
new_op.Parametros.Descricao = $"{new_op.Parametros.Modo} - Padrão";
|
||||
Parametros = new_op.Parametros;
|
||||
if (VariaveisControleOperacao.RoverEmFoco != null)
|
||||
VariaveisControleOperacao.RoverEmFoco.Modo = modo;
|
||||
}
|
||||
|
||||
private void AtualizarModoOperacaoAtual(AgroBase.Models.Enums.ModoOperacao modo)
|
||||
{
|
||||
if (
|
||||
modoOperacaoAtual.HasValue &&
|
||||
modoOperacaoAtual.Value == modo
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
modoOperacaoAtual =
|
||||
modo;
|
||||
|
||||
|
||||
ModoOperacaoAlterado?
|
||||
.Invoke(
|
||||
modo
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private void ReiniciarOperacao()
|
||||
{
|
||||
VariaveisControleOperacao.EnviarComandoIniciarOperacao(false, false);
|
||||
|
|
@ -75,13 +433,143 @@ namespace OperationControl.ViewModels.Views.Operacao.Monitoramento
|
|||
{
|
||||
if (origem == null) return;
|
||||
Parametros = origem;
|
||||
modoOperacaoAtual = origem.Modo;
|
||||
OperacaoConcluida = (VariaveisControleOperacao.RoverEmFoco?.DadosLeitura?.Operacao?.Status ?? AgroBase.Models.Enums.StatusOperacao.NaoIniciado) == AgroBase.Models.Enums.StatusOperacao.Concluido;
|
||||
}
|
||||
|
||||
public void ProcessarStreetMapClicked(MapViewControl.StreetMapClickedEventArgs e)
|
||||
public void AtualizarEstadoMapa(
|
||||
int quantidadeRuas,
|
||||
int quantidadeRuasPercurso,
|
||||
bool edicaoAtiva,
|
||||
int quantidadeSelecionadasEdicao,
|
||||
bool edicaoModificada,
|
||||
int quantidadeCorredores,
|
||||
double distanciaOperacaoMetros,
|
||||
double areaOperacaoHectares,
|
||||
double larguraMediaCorredorMetros,
|
||||
double? tempoEstimadoOperacaoSegundos)
|
||||
{
|
||||
_quantidadeRuasMapa =
|
||||
Math.Max(
|
||||
0,
|
||||
quantidadeRuas
|
||||
);
|
||||
|
||||
|
||||
_quantidadeRuasPercurso =
|
||||
Math.Max(
|
||||
0,
|
||||
quantidadeRuasPercurso
|
||||
);
|
||||
|
||||
|
||||
_edicaoMapaAtiva =
|
||||
edicaoAtiva;
|
||||
|
||||
|
||||
_quantidadeRuasSelecionadasEdicao =
|
||||
Math.Max(
|
||||
0,
|
||||
quantidadeSelecionadasEdicao
|
||||
);
|
||||
|
||||
|
||||
_edicaoMapaModificada =
|
||||
edicaoModificada;
|
||||
|
||||
|
||||
QuantidadeCorredores =
|
||||
quantidadeCorredores;
|
||||
|
||||
|
||||
DistanciaOperacaoMetros =
|
||||
distanciaOperacaoMetros;
|
||||
|
||||
|
||||
AreaOperacaoHectares =
|
||||
areaOperacaoHectares;
|
||||
|
||||
|
||||
LarguraMediaCorredorMetros =
|
||||
larguraMediaCorredorMetros;
|
||||
|
||||
|
||||
TempoEstimadoOperacaoSegundos =
|
||||
tempoEstimadoOperacaoSegundos;
|
||||
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(QuantidadeRuasMapa)
|
||||
);
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(QuantidadeRuasPercurso)
|
||||
);
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(QuantidadeRuasSelecionadasEdicao)
|
||||
);
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(EdicaoMapaAtiva)
|
||||
);
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(EdicaoMapaModificada)
|
||||
);
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(PossuiMapaOperacional)
|
||||
);
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(PodeEditarMapa)
|
||||
);
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(MapaResumoTexto)
|
||||
);
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(EstadoEdicaoMapaTexto)
|
||||
);
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(SelecaoEdicaoMapaTexto)
|
||||
);
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(
|
||||
PossuiEstimativaMapa
|
||||
)
|
||||
);
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(
|
||||
MapaCorredoresTexto
|
||||
)
|
||||
);
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(
|
||||
MapaDistanciaTexto
|
||||
)
|
||||
);
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(
|
||||
MapaAreaTexto
|
||||
)
|
||||
);
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(
|
||||
MapaTempoEstimadoTexto
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
CommandManager
|
||||
.InvalidateRequerySuggested();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -118,30 +118,6 @@ namespace OperationControl.ViewModels.Views.Operacao
|
|||
}
|
||||
|
||||
|
||||
|
||||
public void ProcessarMarkerClicked(MapMarkerManager.MarkerClickedEventArgs e)
|
||||
{
|
||||
if (string.IsNullOrEmpty(e.MarkerId))
|
||||
{
|
||||
Variaveis.MostrarLog("Sem rover em foco");
|
||||
RoverSelecionadoChanged?.Invoke(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
Variaveis.MostrarLog($"Rover em foco: {e.MarkerId}");
|
||||
RoverSelecionadoChanged?.Invoke(e.MarkerId);
|
||||
}
|
||||
}
|
||||
|
||||
public void ProcessarStreetMapClicked(MapViewControl.StreetMapClickedEventArgs e)
|
||||
{
|
||||
Variaveis.MostrarLog($"Rua clicada: {e.StreetId}. Ruas selecionadas: {string.Join(",", e.SelectedStreetsIds)}");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
public enum OperacaoCenterConteudo
|
||||
|
|
|
|||
|
|
@ -339,31 +339,178 @@ namespace OperationControl.ViewModels.Views.Operacao
|
|||
}
|
||||
else
|
||||
{
|
||||
if (((dadosRover?.Operacao?.Modo ?? AgroBase.Models.Enums.ModoOperacao.NaoDefinido) == AgroBase.Models.Enums.ModoOperacao.MapaGPS) && !(VariaveisControleOperacao.RoverEmFoco?.RuasPercorrer?.Any() ?? false))
|
||||
/*
|
||||
* =========================================================
|
||||
* 1. VALIDAÇÃO DA PARAMETRIZAÇÃO
|
||||
* =========================================================
|
||||
*/
|
||||
|
||||
if (
|
||||
(
|
||||
dadosRover?.Operacao?.Modo ??
|
||||
AgroBase.Models.Enums.ModoOperacao.NaoDefinido
|
||||
)
|
||||
==
|
||||
AgroBase.Models.Enums.ModoOperacao.MapaGPS
|
||||
&&
|
||||
!(
|
||||
VariaveisControleOperacao
|
||||
.RoverEmFoco?
|
||||
.RuasPercorrer?
|
||||
.Any()
|
||||
??
|
||||
false
|
||||
)
|
||||
)
|
||||
{
|
||||
System.Windows.MessageBox.Show("Para iniciar a operação automática, é necessário carregar um mapa e selecionar as ruas onde o equipamento irá operar!", "Parametrização", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
System.Windows.MessageBox.Show(
|
||||
"Para iniciar a operação automática, " +
|
||||
"é necessário carregar um mapa e selecionar " +
|
||||
"as ruas onde o equipamento irá operar!",
|
||||
"Parametrização",
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Warning
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (System.Windows.MessageBox.Show("Deseja iniciar a operação com os parâmetros atuais?", "Iniciar Operação", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
|
||||
|
||||
/*
|
||||
* =========================================================
|
||||
* 2. AUTORIZAÇÃO DA ESTAÇÃO GNSS
|
||||
* =========================================================
|
||||
*
|
||||
* O Left Bar não conhece as regras da estação.
|
||||
* Ele apenas pergunta ao Dock se o início está liberado.
|
||||
*/
|
||||
|
||||
bool inicioAutorizado =
|
||||
Variaveis.Dock?
|
||||
._vm?
|
||||
.AutorizarInicioOperacao()
|
||||
??
|
||||
false;
|
||||
|
||||
|
||||
if (!inicioAutorizado)
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
Variaveis.Dock?._vm?._viewOperacaoCenter?.Mapa?.LimparRastrosRover(VariaveisControleOperacao.RoverEmFoco?.RoverId);
|
||||
if ((dadosRover?.Operacao?.Status ?? StatusOperacao.NaoIniciado) == StatusOperacao.Concluido)
|
||||
{
|
||||
VariaveisControleOperacao.EnviarComandoIniciarOperacao(false, false);
|
||||
await Task.Delay(2000);
|
||||
}
|
||||
bool simulador = AppShell.Mock || AppShell.AddRover; // || VariaveisControleOperacao.RoverEmFoco?.DadosLeitura?.Gnss?.Iniciado != true;
|
||||
VariaveisControleOperacao.EnviarComandoIniciarOperacao(true, simulador);
|
||||
await Task.Delay(1000);
|
||||
if (!(dadosRover?.Operacao?.Liberada ?? false))
|
||||
{
|
||||
System.Windows.MessageBox.Show($"{VariaveisControleOperacao.RoverEmFoco?.DadosLeitura?.Operacao?.ImpedimentoErro}", "Operação iniciada com impedimento!", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* =========================================================
|
||||
* 3. CONFIRMAÇÃO HUMANA
|
||||
* =========================================================
|
||||
*/
|
||||
|
||||
if (
|
||||
System.Windows.MessageBox.Show(
|
||||
"Deseja iniciar a operação com os parâmetros atuais?",
|
||||
"Iniciar Operação",
|
||||
MessageBoxButton.YesNo,
|
||||
MessageBoxImage.Question
|
||||
)
|
||||
!=
|
||||
MessageBoxResult.Yes
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* =========================================================
|
||||
* 4. FLUXO DE INÍCIO JÁ EXISTENTE
|
||||
* =========================================================
|
||||
*/
|
||||
|
||||
Task.Run(
|
||||
async () =>
|
||||
{
|
||||
Variaveis.Dock?
|
||||
._vm?
|
||||
.LimparMovimentoRover(
|
||||
VariaveisControleOperacao
|
||||
.RoverEmFoco?
|
||||
.RoverId
|
||||
);
|
||||
|
||||
|
||||
/*
|
||||
* Se a operação anterior terminou,
|
||||
* primeiro reseta seu estado.
|
||||
*/
|
||||
if (
|
||||
(
|
||||
dadosRover?
|
||||
.Operacao?
|
||||
.Status
|
||||
??
|
||||
StatusOperacao.NaoIniciado
|
||||
)
|
||||
==
|
||||
StatusOperacao.Concluido
|
||||
)
|
||||
{
|
||||
VariaveisControleOperacao
|
||||
.EnviarComandoIniciarOperacao(
|
||||
false,
|
||||
false
|
||||
);
|
||||
|
||||
|
||||
await Task.Delay(
|
||||
2000
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Mock / AddRover continuam funcionando
|
||||
* normalmente como simulador.
|
||||
*/
|
||||
bool simulador =
|
||||
AppShell.Mock ||
|
||||
AppShell.AddRover;
|
||||
|
||||
|
||||
VariaveisControleOperacao
|
||||
.EnviarComandoIniciarOperacao(
|
||||
true,
|
||||
simulador
|
||||
);
|
||||
|
||||
|
||||
await Task.Delay(
|
||||
1000
|
||||
);
|
||||
|
||||
|
||||
/*
|
||||
* Depois do comando ainda preservamos
|
||||
* a validação que já existia no rover.
|
||||
*/
|
||||
if (
|
||||
!(
|
||||
dadosRover?
|
||||
.Operacao?
|
||||
.Liberada
|
||||
??
|
||||
false
|
||||
)
|
||||
)
|
||||
{
|
||||
System.Windows.MessageBox.Show(
|
||||
$"{VariaveisControleOperacao.RoverEmFoco?.DadosLeitura?.Operacao?.ImpedimentoErro}",
|
||||
"Operação iniciada com impedimento!",
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Warning
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -386,7 +533,7 @@ namespace OperationControl.ViewModels.Views.Operacao
|
|||
|
||||
private void ReconfigurarGnss()
|
||||
{
|
||||
Variaveis.Dock._vm.CurrentStep = DockStep.PreparacaoMapa;
|
||||
Variaveis.Dock?._vm?.IrParaConfiguracaoGnss();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using System.Globalization;
|
|||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using static OperationControl.Services.BaseFixService;
|
||||
using static OperationControl.Services.GpsService;
|
||||
|
||||
namespace OperationControl.ViewModels
|
||||
{
|
||||
|
|
@ -26,28 +27,60 @@ namespace OperationControl.ViewModels
|
|||
public event Action<MetodoFixacaoBase, double?, double?, double?>? FixarBaseSolicitado;
|
||||
// Offset frontal cm, offset lateral cm
|
||||
public event Action<double?, double?>? AplicarOffsetSolicitado;
|
||||
public event Action? UsarGnssMovelSolicitado;
|
||||
public event Action? ContinuarSolicitado;
|
||||
public event Action? VoltarSolicitado;
|
||||
|
||||
public List<MetodoFixacaoItem> MetodosFixacaoDisponiveis { get; } = new()
|
||||
{
|
||||
new MetodoFixacaoItem { Valor = MetodoFixacaoBase.Ntrip, Nome = "Utilizando NTRIP" },
|
||||
new MetodoFixacaoItem { Valor = MetodoFixacaoBase.SurveyIn, Nome = "Modo Survey-In" },
|
||||
new MetodoFixacaoItem { Valor = MetodoFixacaoBase.Manual, Nome = "Preenchimento manual" }
|
||||
new MetodoFixacaoItem
|
||||
{
|
||||
Valor =
|
||||
MetodoFixacaoBase.Ntrip,
|
||||
|
||||
Nome =
|
||||
"Correção pela rede (NTRIP)"
|
||||
},
|
||||
|
||||
new MetodoFixacaoItem
|
||||
{
|
||||
Valor =
|
||||
MetodoFixacaoBase.SurveyIn,
|
||||
|
||||
Nome =
|
||||
"Determinar posição no local"
|
||||
},
|
||||
|
||||
new MetodoFixacaoItem
|
||||
{
|
||||
Valor =
|
||||
MetodoFixacaoBase.Manual,
|
||||
|
||||
Nome =
|
||||
"Informar posição conhecida"
|
||||
}
|
||||
};
|
||||
|
||||
public ICommand CarregarMapaCommand { get; }
|
||||
public ICommand FixarBaseCommand { get; }
|
||||
public ICommand AplicarOffsetCommand { get; }
|
||||
public ICommand UsarGnssMovelCommand { get; }
|
||||
public ICommand ContinuarCommand { get; }
|
||||
public ICommand VoltarCommand { get; }
|
||||
|
||||
public PreparacaoMapaBottomViewModel()
|
||||
{
|
||||
CarregarMapaCommand = new RelayCommand(_ => ExecutarCarregarMapa(), _ => PodeCarregarMapa);
|
||||
FixarBaseCommand = new RelayCommand(_ => ExecutarFixarBase(), _ => PodeFixarBase);
|
||||
AplicarOffsetCommand = new RelayCommand(_ => ExecutarAplicarOffset(), _ => PodeEditarOffsetBase);
|
||||
UsarGnssMovelCommand = new RelayCommand(_ => ExecutarUsarGnssMovel(), _ => PodeUsarGnssMovel);
|
||||
ContinuarCommand = new RelayCommand(_ => ContinuarSolicitado?.Invoke(), _ => PodeContinuar);
|
||||
VoltarCommand = new RelayCommand(_ => VoltarSolicitado?.Invoke());
|
||||
|
||||
MetodoFixacaoSelecionado = MetodoFixacaoBase.Ntrip;
|
||||
OffsetFrontalBaseTexto = "0";
|
||||
OffsetLateralBaseTexto = "0";
|
||||
StatusTexto = "Pronto para carregar o mapa e fixar a base.";
|
||||
StatusTexto = "Carregue o mapa da operação e prepare o GNSS.";
|
||||
}
|
||||
|
||||
private MetodoFixacaoBase _metodoFixacaoSelecionado = MetodoFixacaoBase.Ntrip;
|
||||
|
|
@ -85,6 +118,258 @@ namespace OperationControl.ViewModels
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
private bool _mapaCarregado;
|
||||
|
||||
public bool PodeContinuar =>
|
||||
_mapaCarregado &&
|
||||
!FixacaoEmAndamento &&
|
||||
!AlteracaoPapelGnssEmAndamento;
|
||||
|
||||
public bool PodeVoltar =>
|
||||
!FixacaoEmAndamento &&
|
||||
!AlteracaoPapelGnssEmAndamento;
|
||||
|
||||
public void DefinirMapaCarregado(bool carregado)
|
||||
{
|
||||
if (
|
||||
_mapaCarregado ==
|
||||
carregado
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
_mapaCarregado =
|
||||
carregado;
|
||||
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(PodeContinuar)
|
||||
);
|
||||
}
|
||||
|
||||
private GnssPapelEstacao _papelGnssAtual = GnssPapelEstacao.Indefinido;
|
||||
public GnssPapelEstacao PapelGnssAtual
|
||||
{
|
||||
get =>
|
||||
_papelGnssAtual;
|
||||
|
||||
private set
|
||||
{
|
||||
if (
|
||||
_papelGnssAtual ==
|
||||
value
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
_papelGnssAtual =
|
||||
value;
|
||||
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(PapelGnssAtual)
|
||||
);
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(PapelGnssTexto)
|
||||
);
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(PapelGnssDescricao)
|
||||
);
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(PapelGnssCor)
|
||||
);
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(TextoBotaoModoGnss)
|
||||
);
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(PodeUsarGnssMovel)
|
||||
);
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(TextoBotaoFixarBase)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private bool _alteracaoPapelGnssEmAndamento;
|
||||
|
||||
public bool AlteracaoPapelGnssEmAndamento {
|
||||
get =>
|
||||
_alteracaoPapelGnssEmAndamento;
|
||||
|
||||
private set
|
||||
{
|
||||
if (
|
||||
_alteracaoPapelGnssEmAndamento ==
|
||||
value
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
_alteracaoPapelGnssEmAndamento =
|
||||
value;
|
||||
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(
|
||||
AlteracaoPapelGnssEmAndamento
|
||||
)
|
||||
);
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(PapelGnssTexto)
|
||||
);
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(PapelGnssDescricao)
|
||||
);
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(PapelGnssCor)
|
||||
);
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(PodeUsarGnssMovel)
|
||||
);
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(PodeCarregarMapa)
|
||||
);
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(PodeEditarMetodoFixacao)
|
||||
);
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(PodeEditarCoordenadasManuais)
|
||||
);
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(PodeEditarOffsetBase)
|
||||
);
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(PodeFixarBase)
|
||||
);
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(PodeContinuar)
|
||||
);
|
||||
|
||||
OnPropertyChanged(
|
||||
nameof(PodeVoltar)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public string PapelGnssTexto
|
||||
{
|
||||
get
|
||||
{
|
||||
if (AlteracaoPapelGnssEmAndamento)
|
||||
return "CONFIGURANDO...";
|
||||
|
||||
|
||||
if (FixacaoEmAndamento)
|
||||
return "FIXANDO BASE...";
|
||||
|
||||
|
||||
return PapelGnssAtual switch
|
||||
{
|
||||
GnssPapelEstacao.Movel =>
|
||||
"LEVANTAMENTO MÓVEL",
|
||||
|
||||
GnssPapelEstacao.BaseFixa =>
|
||||
"BASE FIXA",
|
||||
|
||||
_ =>
|
||||
"NÃO DEFINIDO"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public string PapelGnssDescricao
|
||||
{
|
||||
get
|
||||
{
|
||||
if (AlteracaoPapelGnssEmAndamento)
|
||||
{
|
||||
return
|
||||
"Aguarde enquanto o modo do receptor GNSS é alterado.";
|
||||
}
|
||||
|
||||
|
||||
if (FixacaoEmAndamento)
|
||||
{
|
||||
return
|
||||
"A posição da estação está sendo determinada.";
|
||||
}
|
||||
|
||||
|
||||
return PapelGnssAtual switch
|
||||
{
|
||||
GnssPapelEstacao.Movel =>
|
||||
"A antena pode ser movimentada e usada para levantamento e edição do mapa.",
|
||||
|
||||
GnssPapelEstacao.BaseFixa =>
|
||||
"A estação está fixa e pronta para fornecer referência à operação.",
|
||||
|
||||
_ =>
|
||||
"Escolha o modo móvel para levantamento ou fixe a estação como base."
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public Brush PapelGnssCor =>
|
||||
AlteracaoPapelGnssEmAndamento ||
|
||||
FixacaoEmAndamento
|
||||
? Brushes.Gold
|
||||
|
||||
: PapelGnssAtual switch
|
||||
{
|
||||
GnssPapelEstacao.Movel =>
|
||||
Brushes.DeepSkyBlue,
|
||||
|
||||
GnssPapelEstacao.BaseFixa =>
|
||||
Brushes.LimeGreen,
|
||||
|
||||
_ =>
|
||||
Brushes.Gray
|
||||
};
|
||||
|
||||
public string TextoBotaoModoGnss =>
|
||||
PapelGnssAtual switch
|
||||
{
|
||||
GnssPapelEstacao.BaseFixa =>
|
||||
"Liberar para levantamento",
|
||||
|
||||
GnssPapelEstacao.Movel =>
|
||||
"Levantamento ativo",
|
||||
|
||||
_ =>
|
||||
"Usar para levantamento"
|
||||
};
|
||||
|
||||
public bool PodeUsarGnssMovel =>
|
||||
!FixacaoEmAndamento &&
|
||||
!AlteracaoPapelGnssEmAndamento &&
|
||||
PapelGnssAtual !=
|
||||
GnssPapelEstacao.Movel;
|
||||
|
||||
|
||||
|
||||
private bool _fixacaoEmAndamento;
|
||||
public bool FixacaoEmAndamento
|
||||
{
|
||||
|
|
@ -102,18 +387,45 @@ namespace OperationControl.ViewModels
|
|||
OnPropertyChanged(nameof(PodeEditarOffsetBase));
|
||||
OnPropertyChanged(nameof(PodeFixarBase));
|
||||
OnPropertyChanged(nameof(TextoBotaoFixarBase));
|
||||
OnPropertyChanged(
|
||||
nameof(PapelGnssTexto)
|
||||
);
|
||||
OnPropertyChanged(
|
||||
nameof(PapelGnssDescricao)
|
||||
);
|
||||
OnPropertyChanged(
|
||||
nameof(PapelGnssCor)
|
||||
);
|
||||
OnPropertyChanged(
|
||||
nameof(PodeUsarGnssMovel)
|
||||
);
|
||||
OnPropertyChanged(
|
||||
nameof(PodeContinuar)
|
||||
);
|
||||
OnPropertyChanged(
|
||||
nameof(PodeVoltar)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool PodeCarregarMapa => !FixacaoEmAndamento;
|
||||
public bool PodeCarregarMapa =>
|
||||
!FixacaoEmAndamento &&
|
||||
!AlteracaoPapelGnssEmAndamento;
|
||||
|
||||
public bool PodeEditarMetodoFixacao => !FixacaoEmAndamento;
|
||||
public bool PodeEditarMetodoFixacao =>
|
||||
!FixacaoEmAndamento &&
|
||||
!AlteracaoPapelGnssEmAndamento;
|
||||
|
||||
public bool PodeEditarCoordenadasManuais =>
|
||||
!FixacaoEmAndamento && MetodoFixacaoSelecionado == MetodoFixacaoBase.Manual;
|
||||
!FixacaoEmAndamento &&
|
||||
!AlteracaoPapelGnssEmAndamento &&
|
||||
MetodoFixacaoSelecionado ==
|
||||
MetodoFixacaoBase.Manual;
|
||||
|
||||
public bool PodeEditarOffsetBase => !FixacaoEmAndamento;
|
||||
public bool PodeEditarOffsetBase =>
|
||||
!FixacaoEmAndamento &&
|
||||
!AlteracaoPapelGnssEmAndamento;
|
||||
|
||||
public bool ExibirCamposManuais => MetodoFixacaoSelecionado == MetodoFixacaoBase.Manual;
|
||||
|
||||
|
|
@ -134,6 +446,9 @@ namespace OperationControl.ViewModels
|
|||
{
|
||||
get
|
||||
{
|
||||
if (AlteracaoPapelGnssEmAndamento)
|
||||
return false;
|
||||
|
||||
if (FixacaoEmAndamento)
|
||||
return true; // botão vira "Parar"
|
||||
|
||||
|
|
@ -153,9 +468,23 @@ namespace OperationControl.ViewModels
|
|||
if (FixacaoEmAndamento)
|
||||
return "Parar";
|
||||
|
||||
return MetodoFixacaoSelecionado == MetodoFixacaoBase.Manual
|
||||
? "Aplicar posição manual"
|
||||
: "Fixar posição da base";
|
||||
|
||||
if (
|
||||
MetodoFixacaoSelecionado ==
|
||||
MetodoFixacaoBase.Manual
|
||||
)
|
||||
{
|
||||
return PapelGnssAtual ==
|
||||
GnssPapelEstacao.BaseFixa
|
||||
? "Reposicionar base"
|
||||
: "Aplicar posição da base";
|
||||
}
|
||||
|
||||
|
||||
return PapelGnssAtual ==
|
||||
GnssPapelEstacao.BaseFixa
|
||||
? "Reposicionar base"
|
||||
: "Fixar como base";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -452,6 +781,68 @@ namespace OperationControl.ViewModels
|
|||
AltitudeManual = null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void ExecutarUsarGnssMovel()
|
||||
{
|
||||
if (!PodeUsarGnssMovel)
|
||||
return;
|
||||
|
||||
UsarGnssMovelSolicitado?.Invoke();
|
||||
}
|
||||
|
||||
public void IniciarAlteracaoPapelGnss()
|
||||
{
|
||||
AlteracaoPapelGnssEmAndamento =
|
||||
true;
|
||||
|
||||
|
||||
StatusTexto =
|
||||
"Configurando o GNSS para levantamento...";
|
||||
}
|
||||
|
||||
public void FinalizarAlteracaoPapelGnss(bool sucesso, GnssPapelEstacao papel, string mensagem)
|
||||
{
|
||||
PapelGnssAtual =
|
||||
papel;
|
||||
|
||||
|
||||
AlteracaoPapelGnssEmAndamento =
|
||||
false;
|
||||
|
||||
|
||||
if (
|
||||
papel !=
|
||||
GnssPapelEstacao.BaseFixa
|
||||
)
|
||||
{
|
||||
ExibirOffsets =
|
||||
false;
|
||||
}
|
||||
|
||||
|
||||
StatusTexto =
|
||||
mensagem;
|
||||
}
|
||||
|
||||
public void AtualizarPapelGnss(GnssPapelEstacao papel)
|
||||
{
|
||||
PapelGnssAtual =
|
||||
papel;
|
||||
|
||||
|
||||
if (
|
||||
papel !=
|
||||
GnssPapelEstacao.BaseFixa
|
||||
)
|
||||
{
|
||||
ExibirOffsets =
|
||||
false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private double? TryParseDouble(string? texto)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(texto))
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ namespace OperationControl.ViewModels
|
|||
set { _headingTexto = value; OnPropertyChanged(nameof(HeadingTexto)); }
|
||||
}
|
||||
|
||||
private string _fixTexto = "Sem fix";
|
||||
private string _fixTexto = "Sem leitura";
|
||||
public string FixTexto
|
||||
{
|
||||
get => _fixTexto;
|
||||
|
|
@ -74,7 +74,7 @@ namespace OperationControl.ViewModels
|
|||
public PreparacaoMapaTopViewModel()
|
||||
{
|
||||
// Mock inicial para visualização
|
||||
LatitudeTexto = "-22.172657";
|
||||
/*LatitudeTexto = "-22.172657";
|
||||
LongitudeTexto = "-47.395216";
|
||||
AltitudeTexto = "612.4 m";
|
||||
HeadingTexto = "183°";
|
||||
|
|
@ -82,7 +82,7 @@ namespace OperationControl.ViewModels
|
|||
FixCor = Brushes.LimeGreen;
|
||||
PrecisaoTexto = "1.2 cm";
|
||||
SatelitesTexto = "24";
|
||||
IdadeCorrecaoTexto = "0.8 s";
|
||||
IdadeCorrecaoTexto = "0.8 s";*/
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -145,7 +145,7 @@
|
|||
|
||||
<!-- Área do mapa com overlays -->
|
||||
<Grid Grid.Row="1">
|
||||
<controls:MapViewControl x:Name="MapaMonitoramento" MarkerClicked="MAP_MarkerClicked" StreetMapClicked="MAP_StreetMapClicked" Latitude="-22.1726572492617" Longitude="-47.3952163870556" Scale="5000" />
|
||||
<controls:MapViewControl x:Name="MapaMonitoramento" Latitude="-22.1726572492617" Longitude="-47.3952163870556" Scale="5000" />
|
||||
|
||||
<controls:HeadingIndicator x:Name="areaHeading" Width="130" Height="130" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="2"/>
|
||||
|
||||
|
|
|
|||
|
|
@ -30,19 +30,6 @@ namespace OperationControl.Views.Operacao.Monitoramento
|
|||
_vm.CameraErvasFrameTipoAlterado += _vm_CameraErvasFrameTipoAlterado;
|
||||
}
|
||||
|
||||
#region MAPA
|
||||
|
||||
private void MAP_MarkerClicked(object? sender, MapMarkerManager.MarkerClickedEventArgs e)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private void MAP_StreetMapClicked(object? sender, MapViewControl.StreetMapClickedEventArgs e)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CAMERA FRONTAL
|
||||
|
||||
|
|
|
|||
|
|
@ -68,6 +68,14 @@
|
|||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Opacity" Value="0.45"/>
|
||||
<Setter Property="Cursor" Value="Arrow"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
|
||||
</Style>
|
||||
|
||||
<Style x:Key="ParamCheckBoxStyle" TargetType="CheckBox">
|
||||
|
|
@ -204,6 +212,145 @@
|
|||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- MAPA OPERACIONAL -->
|
||||
<Border Style="{StaticResource SectionCardStyle}">
|
||||
<StackPanel>
|
||||
|
||||
<TextBlock Text="Mapa operacional" Foreground="White" FontSize="15" FontWeight="SemiBold" Margin="0,0,0,10"/>
|
||||
<TextBlock Text="{Binding MapaResumoTexto}" Foreground="#CFCFCF" FontSize="12" TextWrapping="Wrap" Margin="0,0,0,10"/>
|
||||
<!-- ESTIMATIVA DA OPERAÇÃO -->
|
||||
<Border Background="#1B2620" BorderBrush="#385A43" BorderThickness="1" CornerRadius="7" Padding="9" Margin="0,0,0,10">
|
||||
|
||||
<StackPanel>
|
||||
|
||||
<TextBlock
|
||||
Text="{Binding MapaCorredoresTexto}"
|
||||
Foreground="#BFD8C6"
|
||||
FontSize="11"
|
||||
TextWrapping="Wrap"/>
|
||||
|
||||
|
||||
<Grid
|
||||
Margin="0,8,0,0"
|
||||
Visibility="{Binding PossuiEstimativaMapa,
|
||||
Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Text="Percurso"
|
||||
Foreground="#9A9A9A"
|
||||
FontSize="11"/>
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Text="{Binding MapaDistanciaTexto}"
|
||||
Foreground="#7EE89D"
|
||||
FontSize="12"
|
||||
FontWeight="SemiBold"/>
|
||||
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Text="Área"
|
||||
Foreground="#9A9A9A"
|
||||
FontSize="11"
|
||||
Margin="0,4,0,0"/>
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Text="{Binding MapaAreaTexto}"
|
||||
Foreground="#7EE89D"
|
||||
FontSize="12"
|
||||
FontWeight="SemiBold"
|
||||
Margin="0,4,0,0"/>
|
||||
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
Text="Tempo estimado"
|
||||
Foreground="#9A9A9A"
|
||||
FontSize="11"
|
||||
Margin="0,4,0,0"/>
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
Text="{Binding MapaTempoEstimadoTexto}"
|
||||
Foreground="#7EE89D"
|
||||
FontSize="12"
|
||||
FontWeight="SemiBold"
|
||||
Margin="0,4,0,0"/>
|
||||
|
||||
</Grid>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- STATUS -->
|
||||
<Border Background="#202020" BorderBrush="#444444" BorderThickness="1" CornerRadius="7" Padding="9" Margin="0,0,0,10">
|
||||
<StackPanel>
|
||||
<TextBlock Text="{Binding EstadoEdicaoMapaTexto}" Foreground="#DADADA" FontSize="12" FontWeight="SemiBold" TextWrapping="Wrap"/>
|
||||
<TextBlock Text="{Binding SelecaoEdicaoMapaTexto}" Foreground="#FF70CA" FontSize="11" Margin="0,5,0,0">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding EdicaoMapaAtiva}" Value="True">
|
||||
<Setter Property="Visibility" Value="Visible"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ENTRAR NA EDIÇÃO -->
|
||||
<Button Content="✎ Editar mapa" Command="{Binding EditarMapaCommand}" Background="#2F5D3A" BorderBrush="#4A8A5B">
|
||||
<Button.Style>
|
||||
<Style TargetType="Button" BasedOn="{StaticResource ActionButtonStyle}">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding EdicaoMapaAtiva}" Value="True">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Button.Style>
|
||||
</Button>
|
||||
|
||||
<!-- SAÍDA TEMPORÁRIA / SEGURA -->
|
||||
<Button Content="✕ Cancelar edição" Command="{Binding CancelarEdicaoMapaCommand}" Background="#5B3030" BorderBrush="#8B4A4A" Margin="0">
|
||||
<Button.Style>
|
||||
<Style TargetType="Button" BasedOn="{StaticResource ActionButtonStyle}">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding EdicaoMapaAtiva}" Value="True">
|
||||
<Setter Property="Visibility" Value="Visible"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Button.Style>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- AÇÕES -->
|
||||
<Border Style="{StaticResource SectionCardStyle}">
|
||||
<StackPanel>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
<!-- MAPA -->
|
||||
<Grid Visibility="{Binding ExibirMapa, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<Border Margin="12" Padding="0" CornerRadius="10" Background="#202020" BorderBrush="#353535" BorderThickness="1" ClipToBounds="True">
|
||||
<controls:MapViewControl x:Name="areaMapa" MarkerClicked="MAP_MarkerClicked" StreetMapClicked="MAP_StreetMapClicked" Latitude="-22.1726572492617" Longitude="-47.3952163870556" Scale="5000" />
|
||||
<controls:MapViewControl x:Name="areaMapa" Latitude="-22.1726572492617" Longitude="-47.3952163870556" Scale="5000" />
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
|
|
|
|||
|
|
@ -23,16 +23,5 @@ namespace OperationControl.Views.Operacao
|
|||
DataContext = _vm;
|
||||
}
|
||||
|
||||
private void MAP_MarkerClicked(object? sender, MapMarkerManager.MarkerClickedEventArgs e)
|
||||
{
|
||||
_vm.ProcessarMarkerClicked(e);
|
||||
}
|
||||
|
||||
private void MAP_StreetMapClicked(object? sender, MapViewControl.StreetMapClickedEventArgs e)
|
||||
{
|
||||
_vm.ProcessarStreetMapClicked(e);
|
||||
((App)System.Windows.Application.Current).Shell.Dock?._vm?._viewOperacaoRight?.areaParametrizacao?._vm?.ProcessarStreetMapClicked(e);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,132 +1,786 @@
|
|||
<UserControl x:Class="OperationControl.Views.PreparacaoMapaBottomView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<UserControl.Resources>
|
||||
<BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
|
||||
|
||||
<Style x:Key="BottomLabelStyle" TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="#AAAAAA"/>
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
<Setter Property="Margin" Value="0,0,6,0"/>
|
||||
<BooleanToVisibilityConverter
|
||||
x:Key="BoolToVisibilityConverter"/>
|
||||
|
||||
|
||||
<!-- ===================================================== -->
|
||||
<!-- SCROLL BAR -->
|
||||
<!-- ===================================================== -->
|
||||
|
||||
<Style x:Key="ThinScrollBar"
|
||||
TargetType="ScrollBar">
|
||||
|
||||
<Setter Property="Width"
|
||||
Value="8"/>
|
||||
|
||||
<Setter Property="Template">
|
||||
|
||||
<Setter.Value>
|
||||
|
||||
<ControlTemplate
|
||||
TargetType="ScrollBar">
|
||||
|
||||
<Grid
|
||||
Width="8"
|
||||
Background="Transparent">
|
||||
|
||||
<Border
|
||||
Margin="2,0,2,0"
|
||||
Background="#2F2F2F"
|
||||
CornerRadius="3"/>
|
||||
|
||||
<Track
|
||||
x:Name="PART_Track"
|
||||
Margin="2,0,2,0"
|
||||
IsDirectionReversed="True">
|
||||
|
||||
<Track.DecreaseRepeatButton>
|
||||
|
||||
<RepeatButton
|
||||
Opacity="0"
|
||||
IsHitTestVisible="False"/>
|
||||
|
||||
</Track.DecreaseRepeatButton>
|
||||
|
||||
|
||||
<Track.Thumb>
|
||||
|
||||
<Thumb>
|
||||
|
||||
<Thumb.Template>
|
||||
|
||||
<ControlTemplate
|
||||
TargetType="Thumb">
|
||||
|
||||
<Border
|
||||
Background="#6B7280"
|
||||
CornerRadius="3"
|
||||
MinHeight="30"/>
|
||||
|
||||
</ControlTemplate>
|
||||
|
||||
</Thumb.Template>
|
||||
|
||||
</Thumb>
|
||||
|
||||
</Track.Thumb>
|
||||
|
||||
|
||||
<Track.IncreaseRepeatButton>
|
||||
|
||||
<RepeatButton
|
||||
Opacity="0"
|
||||
IsHitTestVisible="False"/>
|
||||
|
||||
</Track.IncreaseRepeatButton>
|
||||
|
||||
</Track>
|
||||
|
||||
</Grid>
|
||||
|
||||
</ControlTemplate>
|
||||
|
||||
</Setter.Value>
|
||||
|
||||
</Setter>
|
||||
|
||||
</Style>
|
||||
|
||||
<Style x:Key="BottomTextBoxStyle" TargetType="TextBox">
|
||||
<Setter Property="Height" Value="30"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
<Setter Property="Padding" Value="8,2"/>
|
||||
<Setter Property="Margin" Value="0,0,10,0"/>
|
||||
<Setter Property="MinWidth" Value="110"/>
|
||||
|
||||
<!-- ===================================================== -->
|
||||
<!-- CARDS -->
|
||||
<!-- ===================================================== -->
|
||||
|
||||
<Style x:Key="SectionCardStyle"
|
||||
TargetType="Border">
|
||||
|
||||
<Setter Property="Background"
|
||||
Value="#242424"/>
|
||||
|
||||
<Setter Property="BorderBrush"
|
||||
Value="#3A3A3A"/>
|
||||
|
||||
<Setter Property="BorderThickness"
|
||||
Value="1"/>
|
||||
|
||||
<Setter Property="CornerRadius"
|
||||
Value="10"/>
|
||||
|
||||
<Setter Property="Padding"
|
||||
Value="14"/>
|
||||
|
||||
<Setter Property="Margin"
|
||||
Value="0,0,0,12"/>
|
||||
|
||||
</Style>
|
||||
|
||||
|
||||
<Style x:Key="SectionTitleStyle"
|
||||
TargetType="TextBlock">
|
||||
|
||||
<Setter Property="Foreground"
|
||||
Value="White"/>
|
||||
|
||||
<Setter Property="FontSize"
|
||||
Value="14"/>
|
||||
|
||||
<Setter Property="FontWeight"
|
||||
Value="SemiBold"/>
|
||||
|
||||
</Style>
|
||||
|
||||
|
||||
<Style x:Key="SectionDescriptionStyle"
|
||||
TargetType="TextBlock">
|
||||
|
||||
<Setter Property="Foreground"
|
||||
Value="#8F8F8F"/>
|
||||
|
||||
<Setter Property="FontSize"
|
||||
Value="11"/>
|
||||
|
||||
<Setter Property="TextWrapping"
|
||||
Value="Wrap"/>
|
||||
|
||||
<Setter Property="Margin"
|
||||
Value="0,4,0,12"/>
|
||||
|
||||
</Style>
|
||||
|
||||
|
||||
<Style x:Key="FieldLabelStyle"
|
||||
TargetType="TextBlock">
|
||||
|
||||
<Setter Property="Foreground"
|
||||
Value="#A8A8A8"/>
|
||||
|
||||
<Setter Property="FontSize"
|
||||
Value="11"/>
|
||||
|
||||
<Setter Property="Margin"
|
||||
Value="0,0,0,5"/>
|
||||
|
||||
</Style>
|
||||
|
||||
|
||||
<Style x:Key="FieldTextBoxStyle"
|
||||
TargetType="TextBox">
|
||||
|
||||
<Setter Property="Height"
|
||||
Value="34"/>
|
||||
|
||||
<Setter Property="Background"
|
||||
Value="#1C1C1C"/>
|
||||
|
||||
<Setter Property="Foreground"
|
||||
Value="White"/>
|
||||
|
||||
<Setter Property="BorderBrush"
|
||||
Value="#484848"/>
|
||||
|
||||
<Setter Property="BorderThickness"
|
||||
Value="1"/>
|
||||
|
||||
<Setter Property="Padding"
|
||||
Value="8,4"/>
|
||||
|
||||
<Setter Property="VerticalContentAlignment"
|
||||
Value="Center"/>
|
||||
|
||||
</Style>
|
||||
|
||||
|
||||
<!-- ===================================================== -->
|
||||
<!-- BOTÕES -->
|
||||
<!-- ===================================================== -->
|
||||
|
||||
<Style x:Key="PanelButtonStyle"
|
||||
TargetType="Button">
|
||||
|
||||
<Setter Property="Height"
|
||||
Value="40"/>
|
||||
|
||||
<Setter Property="Foreground"
|
||||
Value="White"/>
|
||||
|
||||
<Setter Property="Background"
|
||||
Value="#303030"/>
|
||||
|
||||
<Setter Property="BorderBrush"
|
||||
Value="#505050"/>
|
||||
|
||||
<Setter Property="BorderThickness"
|
||||
Value="1"/>
|
||||
|
||||
<Setter Property="FontSize"
|
||||
Value="12"/>
|
||||
|
||||
<Setter Property="FontWeight"
|
||||
Value="SemiBold"/>
|
||||
|
||||
<Setter Property="Cursor"
|
||||
Value="Hand"/>
|
||||
|
||||
<Setter Property="HorizontalContentAlignment"
|
||||
Value="Center"/>
|
||||
|
||||
<Setter Property="VerticalContentAlignment"
|
||||
Value="Center"/>
|
||||
|
||||
<Setter Property="Template">
|
||||
|
||||
<Setter.Value>
|
||||
|
||||
<ControlTemplate
|
||||
TargetType="Button">
|
||||
|
||||
<Border
|
||||
x:Name="ButtonBorder"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="7">
|
||||
|
||||
<ContentPresenter
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
|
||||
|
||||
</Border>
|
||||
|
||||
|
||||
<ControlTemplate.Triggers>
|
||||
|
||||
<Trigger
|
||||
Property="IsMouseOver"
|
||||
Value="True">
|
||||
|
||||
<Setter
|
||||
TargetName="ButtonBorder"
|
||||
Property="Background"
|
||||
Value="#393939"/>
|
||||
|
||||
</Trigger>
|
||||
|
||||
|
||||
<Trigger
|
||||
Property="IsEnabled"
|
||||
Value="False">
|
||||
|
||||
<Setter
|
||||
TargetName="ButtonBorder"
|
||||
Property="Opacity"
|
||||
Value="0.38"/>
|
||||
|
||||
<Setter
|
||||
Property="Cursor"
|
||||
Value="Arrow"/>
|
||||
|
||||
</Trigger>
|
||||
|
||||
</ControlTemplate.Triggers>
|
||||
|
||||
</ControlTemplate>
|
||||
|
||||
</Setter.Value>
|
||||
|
||||
</Setter>
|
||||
|
||||
</Style>
|
||||
|
||||
|
||||
<Style x:Key="PrimaryButtonStyle"
|
||||
TargetType="Button"
|
||||
BasedOn="{StaticResource PanelButtonStyle}">
|
||||
|
||||
<Setter Property="Background"
|
||||
Value="#235B3A"/>
|
||||
|
||||
<Setter Property="BorderBrush"
|
||||
Value="#3E8B5E"/>
|
||||
|
||||
</Style>
|
||||
|
||||
|
||||
<Style x:Key="ContinueButtonStyle"
|
||||
TargetType="Button"
|
||||
BasedOn="{StaticResource PanelButtonStyle}">
|
||||
|
||||
<Setter Property="Height"
|
||||
Value="44"/>
|
||||
|
||||
<Setter Property="Background"
|
||||
Value="#174D35"/>
|
||||
|
||||
<Setter Property="BorderBrush"
|
||||
Value="#43A36A"/>
|
||||
|
||||
<Setter Property="FontSize"
|
||||
Value="13"/>
|
||||
|
||||
</Style>
|
||||
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid Background="#181818" Margin="0">
|
||||
|
||||
<Grid Background="#1C1C1C">
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="64"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- LINHA 1 -->
|
||||
<Grid Grid.Row="0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="240"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Botão carregar mapa -->
|
||||
<Button Grid.Column="0" Content="Carregar mapa" Margin="12,12,8,6" Padding="18,10" MinWidth="150" Command="{Binding CarregarMapaCommand}" IsEnabled="{Binding PodeCarregarMapa}"/>
|
||||
<!-- ===================================================== -->
|
||||
<!-- CABEÇALHO -->
|
||||
<!-- ===================================================== -->
|
||||
|
||||
<!-- Combo método -->
|
||||
<ComboBox Grid.Column="1" Margin="8,12,8,6" VerticalContentAlignment="Center" ItemsSource="{Binding MetodosFixacaoDisponiveis}" DisplayMemberPath="Nome" SelectedValuePath="Valor" SelectedValue="{Binding MetodoFixacaoSelecionado, Mode=TwoWay}" IsEnabled="{Binding PodeEditarMetodoFixacao}"/>
|
||||
<Border
|
||||
Grid.Row="0"
|
||||
Background="#222222"
|
||||
BorderBrush="#343434"
|
||||
BorderThickness="0,0,0,1">
|
||||
|
||||
<!-- Botão fixar -->
|
||||
<Button Grid.Column="2" Content="{Binding TextoBotaoFixarBase}" Margin="8,12,12,6" Padding="18,10" MinWidth="180" Command="{Binding FixarBaseCommand}" IsEnabled="{Binding PodeFixarBase}"/>
|
||||
<Grid Margin="12,0">
|
||||
|
||||
<!-- ÁREA DINÂMICA -->
|
||||
<Grid Grid.Column="3" Margin="0,12,12,6">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Progresso normal -->
|
||||
<Grid Grid.Column="0" Visibility="{Binding ExibirProgresso, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="320"/>
|
||||
<ColumnDefinition Width="50"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ProgressBar Grid.Column="0" Margin="0,8,8,0" Height="22" Minimum="0" Maximum="100" Value="{Binding ProgressoFixacao}"/>
|
||||
<!-- ============================================= -->
|
||||
<!-- VOLTAR -->
|
||||
<!-- ============================================= -->
|
||||
|
||||
<TextBlock Grid.Column="1" Text="{Binding ProgressoFixacaoTexto}" VerticalAlignment="Center" HorizontalAlignment="Center" Foreground="White" FontWeight="SemiBold" FontSize="14" Margin="0,0,8,0"/>
|
||||
</Grid>
|
||||
<Button
|
||||
Grid.Column="0"
|
||||
Width="76"
|
||||
Height="32"
|
||||
Command="{Binding VoltarCommand}"
|
||||
IsEnabled="{Binding PodeVoltar}"
|
||||
VerticalAlignment="Center"
|
||||
Background="#292929"
|
||||
Foreground="#D8D8D8"
|
||||
BorderBrush="#454545"
|
||||
BorderThickness="1"
|
||||
Cursor="Hand"
|
||||
FontSize="11"
|
||||
FontWeight="SemiBold"
|
||||
Content="‹ Voltar">
|
||||
|
||||
<!-- Campos manuais -->
|
||||
<Grid Grid.Column="0" Visibility="{Binding ExibirCamposManuais, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="125"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="125"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="110"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button.Template>
|
||||
|
||||
<TextBlock Grid.Column="0" Text="Latitude (º):" Style="{StaticResource BottomLabelStyle}"/>
|
||||
<ControlTemplate TargetType="Button">
|
||||
|
||||
<TextBox Grid.Column="1" Text="{Binding LatitudeManualTexto, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource BottomTextBoxStyle}" IsEnabled="{Binding PodeEditarCoordenadasManuais}"/>
|
||||
<Border
|
||||
x:Name="Border"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="6">
|
||||
|
||||
<TextBlock Grid.Column="2" Text="Longitude (º):" Style="{StaticResource BottomLabelStyle}"/>
|
||||
<ContentPresenter
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
|
||||
<TextBox Grid.Column="3" Text="{Binding LongitudeManualTexto, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource BottomTextBoxStyle}" IsEnabled="{Binding PodeEditarCoordenadasManuais}"/>
|
||||
</Border>
|
||||
|
||||
<TextBlock Grid.Column="4" Text="Altitude elipsoidal (m):" Style="{StaticResource BottomLabelStyle}"/>
|
||||
|
||||
<TextBox Grid.Column="5" Text="{Binding AltitudeManualTexto, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource BottomTextBoxStyle}" IsEnabled="{Binding PodeEditarCoordenadasManuais}"/>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
|
||||
<!-- offsets -->
|
||||
<Grid Grid.Column="1" Visibility="{Binding ExibirOffsets, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="90"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="90"/>
|
||||
<ColumnDefinition Width="150"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Trigger
|
||||
Property="IsMouseOver"
|
||||
Value="True">
|
||||
|
||||
<TextBlock Grid.Column="0" Text="Offset frontal (cm):" Style="{StaticResource BottomLabelStyle}"/>
|
||||
<Setter
|
||||
TargetName="Border"
|
||||
Property="Background"
|
||||
Value="#353535"/>
|
||||
|
||||
<TextBox Grid.Column="1" Text="{Binding OffsetFrontalBaseTexto, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource BottomTextBoxStyle}" IsEnabled="{Binding PodeEditarOffsetBase}"/>
|
||||
<Setter
|
||||
TargetName="Border"
|
||||
Property="BorderBrush"
|
||||
Value="#5A5A5A"/>
|
||||
|
||||
<TextBlock Grid.Column="2" Text="Offset lateral (cm):" Style="{StaticResource BottomLabelStyle}"/>
|
||||
</Trigger>
|
||||
|
||||
<TextBox Grid.Column="3" Text="{Binding OffsetLateralBaseTexto, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource BottomTextBoxStyle}" IsEnabled="{Binding PodeEditarOffsetBase}"/>
|
||||
|
||||
<Button Grid.Column="4" Content="Aplicar Offset" Margin="12,0,12,0" Padding="5,5" MinWidth="120" Command="{Binding AplicarOffsetCommand}" />
|
||||
<Trigger
|
||||
Property="IsEnabled"
|
||||
Value="False">
|
||||
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Setter
|
||||
TargetName="Border"
|
||||
Property="Opacity"
|
||||
Value="0.35"/>
|
||||
|
||||
<!-- LINHA 2 -->
|
||||
<Grid Grid.Row="1" Margin="12,4,12,10">
|
||||
</Trigger>
|
||||
|
||||
<Border Grid.Row="0" Padding="2" Background="#222" CornerRadius="6">
|
||||
</ControlTemplate.Triggers>
|
||||
|
||||
</ControlTemplate>
|
||||
|
||||
</Button.Template>
|
||||
|
||||
</Button>
|
||||
|
||||
|
||||
<!-- ============================================= -->
|
||||
<!-- CONTEXTO -->
|
||||
<!-- ============================================= -->
|
||||
|
||||
<StackPanel
|
||||
Grid.Column="1"
|
||||
Margin="12,0,0,0"
|
||||
VerticalAlignment="Center">
|
||||
|
||||
<TextBlock
|
||||
Text="Preparação"
|
||||
Foreground="White"
|
||||
FontSize="17"
|
||||
FontWeight="SemiBold"/>
|
||||
|
||||
<TextBlock
|
||||
Text="Mapa e posicionamento da estação"
|
||||
Foreground="#858585"
|
||||
FontSize="11"
|
||||
Margin="0,2,0,0"/>
|
||||
|
||||
<StackPanel Grid.Column="0">
|
||||
<TextBlock Text="{Binding StatusTexto}" Foreground="White" FontSize="14" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
|
||||
</Border>
|
||||
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
|
||||
</Border>
|
||||
|
||||
|
||||
<!-- ===================================================== -->
|
||||
<!-- CONTEÚDO -->
|
||||
<!-- ===================================================== -->
|
||||
|
||||
<ScrollViewer
|
||||
Grid.Row="1"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
CanContentScroll="False"
|
||||
PanningMode="VerticalOnly">
|
||||
|
||||
<ScrollViewer.Resources>
|
||||
|
||||
<Style
|
||||
TargetType="ScrollBar"
|
||||
BasedOn="{StaticResource ThinScrollBar}"/>
|
||||
|
||||
</ScrollViewer.Resources>
|
||||
|
||||
|
||||
<StackPanel
|
||||
Margin="14,14,8,14">
|
||||
|
||||
|
||||
<!-- ============================================= -->
|
||||
<!-- MAPA -->
|
||||
<!-- ============================================= -->
|
||||
|
||||
<Border
|
||||
Style="{StaticResource SectionCardStyle}">
|
||||
|
||||
<StackPanel>
|
||||
|
||||
<TextBlock
|
||||
Text="Mapa da operação"
|
||||
Style="{StaticResource SectionTitleStyle}"/>
|
||||
|
||||
<TextBlock
|
||||
Text="Carregue o arquivo que representa as ruas da área onde o equipamento irá operar."
|
||||
Style="{StaticResource SectionDescriptionStyle}"/>
|
||||
|
||||
|
||||
<Button
|
||||
Content="▣ Carregar ou trocar mapa"
|
||||
Style="{StaticResource PanelButtonStyle}"
|
||||
Command="{Binding CarregarMapaCommand}"
|
||||
IsEnabled="{Binding PodeCarregarMapa}"/>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
</Border>
|
||||
|
||||
|
||||
<!-- ===================================================== -->
|
||||
<!-- PAPEL DO GNSS -->
|
||||
<!-- ===================================================== -->
|
||||
|
||||
<Border Style="{StaticResource SectionCardStyle}">
|
||||
|
||||
<StackPanel>
|
||||
|
||||
<TextBlock
|
||||
Text="Uso atual do GNSS"
|
||||
Style="{StaticResource SectionTitleStyle}"/>
|
||||
|
||||
|
||||
<TextBlock
|
||||
Text="Defina se a antena será movimentada para levantamento ou utilizada como referência fixa."
|
||||
Style="{StaticResource SectionDescriptionStyle}"/>
|
||||
|
||||
|
||||
<Border
|
||||
Background="#1D1D1D"
|
||||
BorderBrush="#3D3D3D"
|
||||
BorderThickness="1"
|
||||
CornerRadius="7"
|
||||
Padding="10"
|
||||
Margin="0,0,0,12">
|
||||
|
||||
<StackPanel>
|
||||
|
||||
<TextBlock
|
||||
Text="{Binding PapelGnssTexto}"
|
||||
Foreground="{Binding PapelGnssCor}"
|
||||
FontSize="12"
|
||||
FontWeight="Bold"/>
|
||||
|
||||
|
||||
<TextBlock
|
||||
Text="{Binding PapelGnssDescricao}"
|
||||
Foreground="#A0A0A0"
|
||||
FontSize="10"
|
||||
TextWrapping="Wrap"
|
||||
Margin="0,4,0,0"/>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
</Border>
|
||||
|
||||
|
||||
<Button
|
||||
Content="{Binding TextoBotaoModoGnss}"
|
||||
Style="{StaticResource PanelButtonStyle}"
|
||||
Command="{Binding UsarGnssMovelCommand}"
|
||||
IsEnabled="{Binding PodeUsarGnssMovel}"/>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
</Border>
|
||||
|
||||
|
||||
<!-- ============================================= -->
|
||||
<!-- GNSS / BASE -->
|
||||
<!-- ============================================= -->
|
||||
|
||||
<Border
|
||||
Style="{StaticResource SectionCardStyle}">
|
||||
|
||||
<StackPanel>
|
||||
|
||||
<TextBlock
|
||||
Text="Posicionamento da estação"
|
||||
Style="{StaticResource SectionTitleStyle}"/>
|
||||
|
||||
<TextBlock
|
||||
Text="Defina como a posição da estação será estabelecida quando ela for utilizada como base."
|
||||
Style="{StaticResource SectionDescriptionStyle}"/>
|
||||
|
||||
|
||||
<TextBlock
|
||||
Text="Método de posicionamento"
|
||||
Style="{StaticResource FieldLabelStyle}"/>
|
||||
|
||||
|
||||
<ComboBox
|
||||
Height="36"
|
||||
Margin="0,0,0,12"
|
||||
VerticalContentAlignment="Center"
|
||||
ItemsSource="{Binding MetodosFixacaoDisponiveis}"
|
||||
DisplayMemberPath="Nome"
|
||||
SelectedValuePath="Valor"
|
||||
SelectedValue="{Binding MetodoFixacaoSelecionado, Mode=TwoWay}"
|
||||
IsEnabled="{Binding PodeEditarMetodoFixacao}"/>
|
||||
|
||||
|
||||
<!-- ===================================== -->
|
||||
<!-- MANUAL -->
|
||||
<!-- ===================================== -->
|
||||
|
||||
<StackPanel
|
||||
Visibility="{Binding ExibirCamposManuais,
|
||||
Converter={StaticResource BoolToVisibilityConverter}}"
|
||||
Margin="0,0,0,12">
|
||||
|
||||
<TextBlock
|
||||
Text="Latitude"
|
||||
Style="{StaticResource FieldLabelStyle}"/>
|
||||
|
||||
<TextBox
|
||||
Text="{Binding LatitudeManualTexto,
|
||||
Mode=TwoWay,
|
||||
UpdateSourceTrigger=PropertyChanged}"
|
||||
Style="{StaticResource FieldTextBoxStyle}"
|
||||
IsEnabled="{Binding PodeEditarCoordenadasManuais}"
|
||||
Margin="0,0,0,10"/>
|
||||
|
||||
|
||||
<TextBlock
|
||||
Text="Longitude"
|
||||
Style="{StaticResource FieldLabelStyle}"/>
|
||||
|
||||
<TextBox
|
||||
Text="{Binding LongitudeManualTexto,
|
||||
Mode=TwoWay,
|
||||
UpdateSourceTrigger=PropertyChanged}"
|
||||
Style="{StaticResource FieldTextBoxStyle}"
|
||||
IsEnabled="{Binding PodeEditarCoordenadasManuais}"
|
||||
Margin="0,0,0,10"/>
|
||||
|
||||
|
||||
<TextBlock
|
||||
Text="Altitude elipsoidal (m)"
|
||||
Style="{StaticResource FieldLabelStyle}"/>
|
||||
|
||||
<TextBox
|
||||
Text="{Binding AltitudeManualTexto,
|
||||
Mode=TwoWay,
|
||||
UpdateSourceTrigger=PropertyChanged}"
|
||||
Style="{StaticResource FieldTextBoxStyle}"
|
||||
IsEnabled="{Binding PodeEditarCoordenadasManuais}"/>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
|
||||
<!-- ===================================== -->
|
||||
<!-- PROGRESSO -->
|
||||
<!-- ===================================== -->
|
||||
|
||||
<Grid
|
||||
Visibility="{Binding FixacaoEmAndamento,
|
||||
Converter={StaticResource BoolToVisibilityConverter}}"
|
||||
Margin="0,0,0,12">
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="48"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
|
||||
<ProgressBar
|
||||
Grid.Column="0"
|
||||
Height="18"
|
||||
Minimum="0"
|
||||
Maximum="100"
|
||||
Value="{Binding ProgressoFixacao}"
|
||||
Margin="0,0,8,0"/>
|
||||
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
Text="{Binding ProgressoFixacaoTexto}"
|
||||
Foreground="#D0D0D0"
|
||||
FontSize="11"
|
||||
FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
|
||||
</Grid>
|
||||
|
||||
|
||||
<Button
|
||||
Content="{Binding TextoBotaoFixarBase}"
|
||||
Style="{StaticResource PrimaryButtonStyle}"
|
||||
Command="{Binding FixarBaseCommand}"
|
||||
IsEnabled="{Binding PodeFixarBase}"/>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
</Border>
|
||||
|
||||
|
||||
<!-- ============================================= -->
|
||||
<!-- OFFSET -->
|
||||
<!-- ============================================= -->
|
||||
|
||||
<Border
|
||||
Style="{StaticResource SectionCardStyle}"
|
||||
Visibility="{Binding ExibirOffsets,
|
||||
Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
|
||||
<StackPanel>
|
||||
|
||||
<TextBlock
|
||||
Text="Ajuste da antena"
|
||||
Style="{StaticResource SectionTitleStyle}"/>
|
||||
|
||||
<TextBlock
|
||||
Text="Aplique o deslocamento entre o ponto de referência da base e a posição física da antena."
|
||||
Style="{StaticResource SectionDescriptionStyle}"/>
|
||||
|
||||
|
||||
<TextBlock
|
||||
Text="Offset frontal (cm)"
|
||||
Style="{StaticResource FieldLabelStyle}"/>
|
||||
|
||||
<TextBox
|
||||
Text="{Binding OffsetFrontalBaseTexto,
|
||||
Mode=TwoWay,
|
||||
UpdateSourceTrigger=PropertyChanged}"
|
||||
Style="{StaticResource FieldTextBoxStyle}"
|
||||
IsEnabled="{Binding PodeEditarOffsetBase}"
|
||||
Margin="0,0,0,10"/>
|
||||
|
||||
|
||||
<TextBlock
|
||||
Text="Offset lateral (cm)"
|
||||
Style="{StaticResource FieldLabelStyle}"/>
|
||||
|
||||
<TextBox
|
||||
Text="{Binding OffsetLateralBaseTexto,
|
||||
Mode=TwoWay,
|
||||
UpdateSourceTrigger=PropertyChanged}"
|
||||
Style="{StaticResource FieldTextBoxStyle}"
|
||||
IsEnabled="{Binding PodeEditarOffsetBase}"
|
||||
Margin="0,0,0,12"/>
|
||||
|
||||
|
||||
<Button
|
||||
Content="Aplicar ajuste"
|
||||
Style="{StaticResource PanelButtonStyle}"
|
||||
Command="{Binding AplicarOffsetCommand}"/>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
</ScrollViewer>
|
||||
|
||||
|
||||
<!-- ===================================================== -->
|
||||
<!-- AVANÇAR -->
|
||||
<!-- ===================================================== -->
|
||||
|
||||
<Border
|
||||
Grid.Row="2"
|
||||
Background="#202020"
|
||||
BorderBrush="#343434"
|
||||
BorderThickness="0,1,0,0"
|
||||
Padding="14">
|
||||
|
||||
<Button
|
||||
Content="Continuar para parametrização ❯"
|
||||
Style="{StaticResource ContinueButtonStyle}"
|
||||
Command="{Binding ContinuarCommand}"/>
|
||||
|
||||
</Border>
|
||||
|
||||
</Grid>
|
||||
|
||||
</UserControl>
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
<UserControl x:Class="OperationControl.Views.PreparacaoMapaStatusView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<Grid Background="#181818">
|
||||
|
||||
<Border
|
||||
Margin="8,5"
|
||||
Padding="12,7"
|
||||
Background="#202020"
|
||||
BorderBrush="#343434"
|
||||
BorderThickness="1"
|
||||
CornerRadius="7">
|
||||
|
||||
<Grid>
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
|
||||
<Ellipse
|
||||
Grid.Column="0"
|
||||
Width="8"
|
||||
Height="8"
|
||||
Fill="#56B97A"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,10,0"/>
|
||||
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
Text="{Binding StatusTexto}"
|
||||
Foreground="#D0D0D0"
|
||||
FontSize="12"
|
||||
TextWrapping="Wrap"
|
||||
VerticalAlignment="Center"/>
|
||||
|
||||
</Grid>
|
||||
|
||||
</Border>
|
||||
|
||||
</Grid>
|
||||
|
||||
</UserControl>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
using System.Windows.Controls;
|
||||
using UserControl = System.Windows.Controls.UserControl;
|
||||
|
||||
|
||||
namespace OperationControl.Views
|
||||
{
|
||||
public partial class PreparacaoMapaStatusView : UserControl
|
||||
{
|
||||
public PreparacaoMapaStatusView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,73 +1,254 @@
|
|||
<UserControl x:Class="OperationControl.Views.PreparacaoMapaTopView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:OperationControl.Views"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800">
|
||||
<Grid Background="#222222" Margin="0">
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Auto"
|
||||
VerticalScrollBarVisibility="Disabled">
|
||||
<StackPanel Orientation="Horizontal" Margin="8,6">
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Border Background="#2E2E2E" CornerRadius="8" Padding="12" Margin="4,0">
|
||||
<StackPanel>
|
||||
<TextBlock Text="Latitude" Foreground="#BFBFBF" FontSize="11"/>
|
||||
<TextBlock Text="{Binding LatitudeTexto}" Foreground="White" FontSize="16" FontWeight="SemiBold"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<UserControl.Resources>
|
||||
|
||||
<Border Background="#2E2E2E" CornerRadius="8" Padding="12" Margin="4,0">
|
||||
<StackPanel>
|
||||
<TextBlock Text="Longitude" Foreground="#BFBFBF" FontSize="11"/>
|
||||
<TextBlock Text="{Binding LongitudeTexto}" Foreground="White" FontSize="16" FontWeight="SemiBold"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Style x:Key="MetricCardStyle"
|
||||
TargetType="Border">
|
||||
|
||||
<Border Background="#2E2E2E" CornerRadius="8" Padding="12" Margin="4,0">
|
||||
<StackPanel>
|
||||
<TextBlock Text="Altitude" Foreground="#BFBFBF" FontSize="11"/>
|
||||
<TextBlock Text="{Binding AltitudeTexto}" Foreground="White" FontSize="16" FontWeight="SemiBold"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Setter Property="Background"
|
||||
Value="#292929"/>
|
||||
|
||||
<Border Background="#2E2E2E" CornerRadius="8" Padding="12" Margin="4,0">
|
||||
<StackPanel>
|
||||
<TextBlock Text="Heading" Foreground="#BFBFBF" FontSize="11"/>
|
||||
<TextBlock Text="{Binding HeadingTexto}" Foreground="White" FontSize="16" FontWeight="SemiBold"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Setter Property="BorderBrush"
|
||||
Value="#3A3A3A"/>
|
||||
|
||||
<Border Background="#2E2E2E" CornerRadius="8" Padding="12" Margin="4,0">
|
||||
<StackPanel>
|
||||
<TextBlock Text="Fix" Foreground="#BFBFBF" FontSize="11"/>
|
||||
<TextBlock Text="{Binding FixTexto}" Foreground="{Binding FixCor}" FontSize="16" FontWeight="Bold"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Setter Property="BorderThickness"
|
||||
Value="1"/>
|
||||
|
||||
<Border Background="#2E2E2E" CornerRadius="8" Padding="12" Margin="4,0">
|
||||
<StackPanel>
|
||||
<TextBlock Text="Precisão" Foreground="#BFBFBF" FontSize="11"/>
|
||||
<TextBlock Text="{Binding PrecisaoTexto}" Foreground="White" FontSize="16" FontWeight="SemiBold"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Setter Property="CornerRadius"
|
||||
Value="8"/>
|
||||
|
||||
<Border Background="#2E2E2E" CornerRadius="8" Padding="12" Margin="4,0">
|
||||
<StackPanel>
|
||||
<TextBlock Text="Satélites" Foreground="#BFBFBF" FontSize="11"/>
|
||||
<TextBlock Text="{Binding SatelitesTexto}" Foreground="White" FontSize="16" FontWeight="SemiBold"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Setter Property="Padding"
|
||||
Value="12,8"/>
|
||||
|
||||
<Border Background="#2E2E2E" CornerRadius="8" Padding="12" Margin="4,0">
|
||||
<StackPanel>
|
||||
<TextBlock Text="Idade Correção" Foreground="#BFBFBF" FontSize="11"/>
|
||||
<TextBlock Text="{Binding IdadeCorrecaoTexto}" Foreground="White" FontSize="16" FontWeight="SemiBold"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Setter Property="Margin"
|
||||
Value="4,0"/>
|
||||
|
||||
<Setter Property="MinWidth"
|
||||
Value="105"/>
|
||||
|
||||
</Style>
|
||||
|
||||
|
||||
<Style x:Key="MetricLabelStyle"
|
||||
TargetType="TextBlock">
|
||||
|
||||
<Setter Property="Foreground"
|
||||
Value="#8E8E8E"/>
|
||||
|
||||
<Setter Property="FontSize"
|
||||
Value="10"/>
|
||||
|
||||
<Setter Property="FontWeight"
|
||||
Value="SemiBold"/>
|
||||
|
||||
</Style>
|
||||
|
||||
|
||||
<Style x:Key="MetricValueStyle"
|
||||
TargetType="TextBlock">
|
||||
|
||||
<Setter Property="Foreground"
|
||||
Value="White"/>
|
||||
|
||||
<Setter Property="FontSize"
|
||||
Value="15"/>
|
||||
|
||||
<Setter Property="FontWeight"
|
||||
Value="SemiBold"/>
|
||||
|
||||
<Setter Property="Margin"
|
||||
Value="0,3,0,0"/>
|
||||
|
||||
</Style>
|
||||
|
||||
|
||||
<Style x:Key="CoordinateLabelStyle"
|
||||
TargetType="TextBlock">
|
||||
|
||||
<Setter Property="Foreground"
|
||||
Value="#707070"/>
|
||||
|
||||
<Setter Property="FontSize"
|
||||
Value="11"/>
|
||||
|
||||
<Setter Property="Margin"
|
||||
Value="0,0,4,0"/>
|
||||
|
||||
</Style>
|
||||
|
||||
|
||||
<Style x:Key="CoordinateValueStyle"
|
||||
TargetType="TextBlock">
|
||||
|
||||
<Setter Property="Foreground"
|
||||
Value="#BDBDBD"/>
|
||||
|
||||
<Setter Property="FontSize"
|
||||
Value="11"/>
|
||||
|
||||
<Setter Property="FontWeight"
|
||||
Value="SemiBold"/>
|
||||
|
||||
</Style>
|
||||
|
||||
</UserControl.Resources>
|
||||
|
||||
|
||||
<Grid Background="#1C1C1C"
|
||||
MinHeight="92">
|
||||
|
||||
<Grid Margin="16,10">
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
|
||||
<!-- ================================================= -->
|
||||
<!-- CONTEXTO -->
|
||||
<!-- ================================================= -->
|
||||
|
||||
<StackPanel Grid.Column="0"
|
||||
VerticalAlignment="Center">
|
||||
|
||||
<TextBlock
|
||||
Text="PREPARAÇÃO DO MAPA E GNSS"
|
||||
Foreground="White"
|
||||
FontSize="17"
|
||||
FontWeight="Bold"/>
|
||||
|
||||
<TextBlock
|
||||
Text="Carregue o mapa da operação e prepare o posicionamento da estação GNSS."
|
||||
Foreground="#A0A0A0"
|
||||
FontSize="12"
|
||||
Margin="0,3,0,8"/>
|
||||
|
||||
|
||||
<WrapPanel>
|
||||
|
||||
<TextBlock
|
||||
Text="LAT"
|
||||
Style="{StaticResource CoordinateLabelStyle}"/>
|
||||
|
||||
<TextBlock
|
||||
Text="{Binding LatitudeTexto}"
|
||||
Style="{StaticResource CoordinateValueStyle}"/>
|
||||
|
||||
|
||||
<TextBlock
|
||||
Text=" • LON"
|
||||
Style="{StaticResource CoordinateLabelStyle}"
|
||||
Margin="12,0,4,0"/>
|
||||
|
||||
<TextBlock
|
||||
Text="{Binding LongitudeTexto}"
|
||||
Style="{StaticResource CoordinateValueStyle}"/>
|
||||
|
||||
|
||||
<TextBlock
|
||||
Text=" • ALT"
|
||||
Style="{StaticResource CoordinateLabelStyle}"
|
||||
Margin="12,0,4,0"/>
|
||||
|
||||
<TextBlock
|
||||
Text="{Binding AltitudeTexto}"
|
||||
Style="{StaticResource CoordinateValueStyle}"/>
|
||||
|
||||
|
||||
<TextBlock
|
||||
Text=" • DIREÇÃO"
|
||||
Style="{StaticResource CoordinateLabelStyle}"
|
||||
Margin="12,0,4,0"/>
|
||||
|
||||
<TextBlock
|
||||
Text="{Binding HeadingTexto}"
|
||||
Style="{StaticResource CoordinateValueStyle}"/>
|
||||
|
||||
</WrapPanel>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
|
||||
<!-- ================================================= -->
|
||||
<!-- SAÚDE / QUALIDADE GNSS -->
|
||||
<!-- ================================================= -->
|
||||
|
||||
<UniformGrid
|
||||
Grid.Column="1"
|
||||
Rows="1"
|
||||
Columns="4"
|
||||
VerticalAlignment="Center">
|
||||
|
||||
<Border Style="{StaticResource MetricCardStyle}">
|
||||
<StackPanel>
|
||||
|
||||
<TextBlock
|
||||
Text="CORREÇÃO"
|
||||
Style="{StaticResource MetricLabelStyle}"/>
|
||||
|
||||
<TextBlock
|
||||
Text="{Binding FixTexto}"
|
||||
Style="{StaticResource MetricValueStyle}"
|
||||
Foreground="{Binding FixCor}"/>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
|
||||
<Border Style="{StaticResource MetricCardStyle}">
|
||||
<StackPanel>
|
||||
|
||||
<TextBlock
|
||||
Text="PRECISÃO"
|
||||
Style="{StaticResource MetricLabelStyle}"/>
|
||||
|
||||
<TextBlock
|
||||
Text="{Binding PrecisaoTexto}"
|
||||
Style="{StaticResource MetricValueStyle}"/>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
|
||||
<Border Style="{StaticResource MetricCardStyle}">
|
||||
<StackPanel>
|
||||
|
||||
<TextBlock
|
||||
Text="SATÉLITES"
|
||||
Style="{StaticResource MetricLabelStyle}"/>
|
||||
|
||||
<TextBlock
|
||||
Text="{Binding SatelitesTexto}"
|
||||
Style="{StaticResource MetricValueStyle}"/>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
|
||||
<Border Style="{StaticResource MetricCardStyle}">
|
||||
<StackPanel>
|
||||
|
||||
<TextBlock
|
||||
Text="IDADE"
|
||||
Style="{StaticResource MetricLabelStyle}"/>
|
||||
|
||||
<TextBlock
|
||||
Text="{Binding IdadeCorrecaoTexto}"
|
||||
Style="{StaticResource MetricValueStyle}"/>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</UniformGrid>
|
||||
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
</UserControl>
|
||||
|
|
@ -805,7 +805,7 @@
|
|||
|
||||
<Border x:Name="brdMapa" BorderBrush="Black" BorderThickness="1" Margin="465,71,475,116">
|
||||
<Canvas>
|
||||
<controls:MapViewControl x:Name="MAP" MarkerClicked="MAP_MarkerClicked" StreetMapClicked="MAP_StreetMapClicked" MbTilesPath="Data/basemap.mbtiles" Latitude="-22.1726572492617" Longitude="-47.3952163870556" Scale="5000" Height="820" Width="978" HorizontalAlignment="Center" VerticalAlignment="Top" />
|
||||
<controls:MapViewControl x:Name="MAP" MbTilesPath="Data/basemap.mbtiles" Latitude="-22.1726572492617" Longitude="-47.3952163870556" Scale="5000" Height="820" Width="978" HorizontalAlignment="Center" VerticalAlignment="Top" />
|
||||
<controls:HeadingIndicator x:Name="HDG" Width="130" Height="130" Canvas.Left="10" Canvas.Top="10" HorizontalAlignment="Left" VerticalAlignment="Center"/>
|
||||
<controls:AttitudeIndicator x:Name="IMU" Width="160" Height="180" Canvas.Left="10" Canvas.Bottom="10" CriticalLimit="20" WarningLimit="10" HorizontalAlignment="Left" VerticalAlignment="Center"/>
|
||||
</Canvas>
|
||||
|
|
|
|||
|
|
@ -99,52 +99,52 @@ namespace OperationControl.Windows
|
|||
|
||||
public void AtualizarListaDispositivos(List<string> dispositivos)
|
||||
{
|
||||
Application.Current.Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
cmbDispositivo.Items.Clear();
|
||||
foreach (var d in dispositivos)
|
||||
{
|
||||
cmbDispositivo.Items.Add(d);
|
||||
if (!MAP.markers.Added(d))
|
||||
{
|
||||
if (d == VariaveisControleOperacao.BaseMarkerID)
|
||||
{
|
||||
MAP.markers.AddMarker(
|
||||
d,
|
||||
true,
|
||||
lat: Variaveis.GpsService?.UltimaLeitura?.Latitude ?? 0,
|
||||
lon: Variaveis.GpsService?.UltimaLeitura?.Longitude ?? 0,
|
||||
heading: Variaveis.GpsService?.UltimaLeitura?.OrientacaoReal ?? 0,
|
||||
label: "Base"
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
var rover = VariaveisControleOperacao.RoversNaRede.FirstOrDefault(x => x.RoverId == d);
|
||||
if (rover != null)
|
||||
{
|
||||
MAP.markers.AddMarker(
|
||||
d,
|
||||
false,
|
||||
lat: rover.DadosLeitura?.Gnss?.Latitude ?? 0,
|
||||
lon: rover.DadosLeitura?.Gnss?.Longitude ?? 0,
|
||||
heading: rover.DadosLeitura?.Gnss?.OrientacaoReal ?? 0
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
int idx_selected = dispositivos.IndexOf(VariaveisControleOperacao.SelectedRoverId);
|
||||
if (idx_selected > -1)
|
||||
cmbDispositivo.SelectedIndex = idx_selected;
|
||||
}
|
||||
catch (Exception exUi)
|
||||
{
|
||||
Variaveis.MostrarLog($"Erro ao atualizar UI lista dispositivos: {exUi.Message}");
|
||||
}
|
||||
}));
|
||||
//Application.Current.Dispatcher.BeginInvoke(new Action(() =>
|
||||
//{
|
||||
// try
|
||||
// {
|
||||
// cmbDispositivo.Items.Clear();
|
||||
// foreach (var d in dispositivos)
|
||||
// {
|
||||
// cmbDispositivo.Items.Add(d);
|
||||
// if (!MAP.markers.Added(d))
|
||||
// {
|
||||
// if (d == VariaveisControleOperacao.BaseMarkerID)
|
||||
// {
|
||||
// MAP.markers.AddMarker(
|
||||
// d,
|
||||
// true,
|
||||
// lat: Variaveis.GpsService?.UltimaLeitura?.Latitude ?? 0,
|
||||
// lon: Variaveis.GpsService?.UltimaLeitura?.Longitude ?? 0,
|
||||
// heading: Variaveis.GpsService?.UltimaLeitura?.OrientacaoReal ?? 0,
|
||||
// label: "Base"
|
||||
// );
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// var rover = VariaveisControleOperacao.RoversNaRede.FirstOrDefault(x => x.RoverId == d);
|
||||
// if (rover != null)
|
||||
// {
|
||||
// MAP.markers.AddMarker(
|
||||
// d,
|
||||
// false,
|
||||
// lat: rover.DadosLeitura?.Gnss?.Latitude ?? 0,
|
||||
// lon: rover.DadosLeitura?.Gnss?.Longitude ?? 0,
|
||||
// heading: rover.DadosLeitura?.Gnss?.OrientacaoReal ?? 0
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// int idx_selected = dispositivos.IndexOf(VariaveisControleOperacao.SelectedRoverId);
|
||||
// if (idx_selected > -1)
|
||||
// cmbDispositivo.SelectedIndex = idx_selected;
|
||||
// }
|
||||
// catch (Exception exUi)
|
||||
// {
|
||||
// Variaveis.MostrarLog($"Erro ao atualizar UI lista dispositivos: {exUi.Message}");
|
||||
// }
|
||||
//}));
|
||||
}
|
||||
|
||||
private void cmbDispositivo_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
|
||||
|
|
@ -160,7 +160,7 @@ namespace OperationControl.Windows
|
|||
}
|
||||
|
||||
VariaveisControleOperacao.SelectedRoverId = selecionado.ToString();
|
||||
MAP.markers.SetMarkerFocused(selecionado.ToString(), true);
|
||||
//MAP.markers.SetMarkerFocused(selecionado.ToString(), true);
|
||||
AtualizarDadosTela_Telemetria(selecionado.ToString());
|
||||
|
||||
brdResumo.IsEnabled = !dados_base;
|
||||
|
|
@ -207,13 +207,13 @@ namespace OperationControl.Windows
|
|||
var dados_leitura = rover?.DadosLeitura;
|
||||
if (dados_base)
|
||||
{
|
||||
MAP.markers.UpdateMarkerPosition(rover_id, lat: Variaveis.GpsService?.UltimaLeitura?.Latitude, lon: Variaveis.GpsService?.UltimaLeitura?.Longitude, heading: Variaveis.GpsService?.UltimaLeitura?.OrientacaoReal);
|
||||
//MAP.markers.UpdateMarkerPosition(rover_id, lat: Variaveis.GpsService?.UltimaLeitura?.Latitude, lon: Variaveis.GpsService?.UltimaLeitura?.Longitude, heading: Variaveis.GpsService?.UltimaLeitura?.OrientacaoReal);
|
||||
}
|
||||
else if (dados_leitura != null)
|
||||
{
|
||||
var simulacao = dados_leitura.Controle?.SimulacaoMPC?.Select(x => new double[] { x.longitude, x.latitude })?.ToList();
|
||||
MAP.markers.UpdateMarkerPosition(rover_id, lat: dados_leitura.Gnss?.Latitude, lon: dados_leitura.Gnss?.Longitude, heading: dados_leitura.Gnss?.OrientacaoReal, predict: simulacao);
|
||||
MAP.markers.UpdateMarkerInfo(rover_id, status: dados_leitura.Operacao?.Status ?? AgroBase.Models.Enums.StatusOperacao.NaoIniciado);
|
||||
//var simulacao = dados_leitura.Controle?.SimulacaoMPC?.Select(x => new double[] { x.longitude, x.latitude })?.ToList();
|
||||
//MAP.markers.UpdateMarkerPosition(rover_id, lat: dados_leitura.Gnss?.Latitude, lon: dados_leitura.Gnss?.Longitude, heading: dados_leitura.Gnss?.OrientacaoReal, predict: simulacao);
|
||||
//MAP.markers.UpdateMarkerInfo(rover_id, status: dados_leitura.Operacao?.Status ?? AgroBase.Models.Enums.StatusOperacao.NaoIniciado);
|
||||
}
|
||||
|
||||
if (rover != null && dados_leitura?.ModulosSaude != null)
|
||||
|
|
@ -1797,54 +1797,54 @@ namespace OperationControl.Windows
|
|||
|
||||
private void btnParametros_Salvar_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var modo = (CTRL.TgManual.IsChecked ?? false) ? AgroBase.Models.Enums.ModoOperacao.Manual : AgroBase.Models.Enums.ModoOperacao.MapaGPS;
|
||||
|
||||
var rover = VariaveisControleOperacao.RoverEmFoco;
|
||||
|
||||
VariaveisControleOperacao.EnviarParametrosOperacao(new AgroBase.Models.Operacoes.OperacaoParametrosModel()
|
||||
{
|
||||
Modo = modo,
|
||||
Descricao = "Operacao da base",
|
||||
QtdCamerasSolo = 1,
|
||||
QtdBicos = 4,
|
||||
CapacidadeReservatorio = 45,
|
||||
|
||||
Controle = new AgroBase.Models.Operacoes.OperacaoParametrosControleModel()
|
||||
{
|
||||
MovimentoAutomatico = modo != AgroBase.Models.Enums.ModoOperacao.Manual,
|
||||
DirecionalAutomatico = modo != AgroBase.Models.Enums.ModoOperacao.Manual,
|
||||
|
||||
FrenagemAutomaticaAoParar = chbParametros_Frenagem.IsChecked ?? false,
|
||||
MovVelocidadeCErvasPercent = int.Parse(txtParametros_VelCErvas.Text),
|
||||
MovVelocidadeSErvasPercent = int.Parse(txtParametros_VelSErvas.Text),
|
||||
|
||||
ImuParadaPorInclinacao = false,
|
||||
MpcHorizonte = 4,
|
||||
DirAuxilioSonar = false,
|
||||
MovAuxilioSonar = false,
|
||||
AnteciparManobraCorredorM = 10,
|
||||
OakParadaPorObstaculo = chbParametros_ParadaObstaculo.IsChecked ?? false,
|
||||
RegistrarDadosPosProcessamento = false,
|
||||
|
||||
DirAnguloMaximo = int.Parse(txtParametros_AngMax.Text),
|
||||
DirTipoMovimento = AgroBase.Models.Enums.TiposControladorDirecional.MPC,
|
||||
DirVelocidadeMovimento = 50,
|
||||
|
||||
PulverizadorAutomatico = chbParametros_Pulverizador.IsChecked ?? false,
|
||||
AtuPercentualErvasBicoOn = double.Parse(txtParametros_PctErvaOn.Text),
|
||||
AtuPercentualErvasBicoOff = double.Parse(txtParametros_PctErvaOff.Text),
|
||||
AtuDuracaoAtuacao = int.Parse(txtParametros_TempoOn.Text),
|
||||
AtuAlturaAreaPulverizacao = double.Parse(txtParametros_AreaAtuacao.Text),
|
||||
AtuPercentualInicioPulverizacao = double.Parse(txtParametros_RoiAtuacao.Text),
|
||||
AtuPressaoLinha = double.Parse(txtParametros_PressaoLiha.Text),
|
||||
},
|
||||
|
||||
ModulosMandatorios = rover?.ModulosMandatorios,
|
||||
ParametrosMandatorios = rover?.ParametrosMandatorios,
|
||||
|
||||
RuasPercorrer = MAP.RuasMapaCarregado.Where(x => x.Selected).Select(x => x.Id).ToList(),
|
||||
Mapa = MAP.CriarDadosMapa()
|
||||
});
|
||||
//var modo = (CTRL.TgManual.IsChecked ?? false) ? AgroBase.Models.Enums.ModoOperacao.Manual : AgroBase.Models.Enums.ModoOperacao.MapaGPS;
|
||||
//
|
||||
//var rover = VariaveisControleOperacao.RoverEmFoco;
|
||||
//
|
||||
//VariaveisControleOperacao.EnviarParametrosOperacao(new AgroBase.Models.Operacoes.OperacaoParametrosModel()
|
||||
//{
|
||||
// Modo = modo,
|
||||
// Descricao = "Operacao da base",
|
||||
// QtdCamerasSolo = 1,
|
||||
// QtdBicos = 4,
|
||||
// CapacidadeReservatorio = 45,
|
||||
//
|
||||
// Controle = new AgroBase.Models.Operacoes.OperacaoParametrosControleModel()
|
||||
// {
|
||||
// MovimentoAutomatico = modo != AgroBase.Models.Enums.ModoOperacao.Manual,
|
||||
// DirecionalAutomatico = modo != AgroBase.Models.Enums.ModoOperacao.Manual,
|
||||
//
|
||||
// FrenagemAutomaticaAoParar = chbParametros_Frenagem.IsChecked ?? false,
|
||||
// MovVelocidadeCErvasPercent = int.Parse(txtParametros_VelCErvas.Text),
|
||||
// MovVelocidadeSErvasPercent = int.Parse(txtParametros_VelSErvas.Text),
|
||||
//
|
||||
// ImuParadaPorInclinacao = false,
|
||||
// MpcHorizonte = 4,
|
||||
// DirAuxilioSonar = false,
|
||||
// MovAuxilioSonar = false,
|
||||
// AnteciparManobraCorredorM = 10,
|
||||
// OakParadaPorObstaculo = chbParametros_ParadaObstaculo.IsChecked ?? false,
|
||||
// RegistrarDadosPosProcessamento = false,
|
||||
//
|
||||
// DirAnguloMaximo = int.Parse(txtParametros_AngMax.Text),
|
||||
// DirTipoMovimento = AgroBase.Models.Enums.TiposControladorDirecional.MPC,
|
||||
// DirVelocidadeMovimento = 50,
|
||||
//
|
||||
// PulverizadorAutomatico = chbParametros_Pulverizador.IsChecked ?? false,
|
||||
// AtuPercentualErvasBicoOn = double.Parse(txtParametros_PctErvaOn.Text),
|
||||
// AtuPercentualErvasBicoOff = double.Parse(txtParametros_PctErvaOff.Text),
|
||||
// AtuDuracaoAtuacao = int.Parse(txtParametros_TempoOn.Text),
|
||||
// AtuAlturaAreaPulverizacao = double.Parse(txtParametros_AreaAtuacao.Text),
|
||||
// AtuPercentualInicioPulverizacao = double.Parse(txtParametros_RoiAtuacao.Text),
|
||||
// AtuPressaoLinha = double.Parse(txtParametros_PressaoLiha.Text),
|
||||
// },
|
||||
//
|
||||
// ModulosMandatorios = rover?.ModulosMandatorios,
|
||||
// ParametrosMandatorios = rover?.ParametrosMandatorios,
|
||||
//
|
||||
// RuasPercorrer = MAP.RuasMapaCarregado.Where(x => x.Selected).Select(x => x.Id).ToList(),
|
||||
// Mapa = MAP.CriarDadosMapa()
|
||||
//});
|
||||
}
|
||||
|
||||
private void btnParametros_Atualizar_Click(object sender, RoutedEventArgs e)
|
||||
|
|
@ -1856,26 +1856,6 @@ namespace OperationControl.Windows
|
|||
|
||||
#region MAPA
|
||||
|
||||
private void MAP_MarkerClicked(object? sender, MapMarkerManager.MarkerClickedEventArgs e)
|
||||
{
|
||||
if (string.IsNullOrEmpty(e.MarkerId))
|
||||
{
|
||||
Variaveis.MostrarLog($"Sem rover em foco");
|
||||
LimparDadosTela_Telemetria(e.MarkerId);
|
||||
}
|
||||
else
|
||||
{
|
||||
Variaveis.MostrarLog($"Rover em foco: {e.MarkerId}");
|
||||
int idx = cmbDispositivo.Items.IndexOf(e.MarkerId);
|
||||
cmbDispositivo.SelectedIndex = idx;
|
||||
}
|
||||
}
|
||||
|
||||
private void MAP_StreetMapClicked(object? sender, MapViewControl.StreetMapClickedEventArgs e)
|
||||
{
|
||||
Variaveis.MostrarLog($"Rua clicada: {e.StreetId}. Ruas selecionadas: {string.Join(",", e.SelectedStreetsIds)}");
|
||||
}
|
||||
|
||||
private void AtualizarDadosTela_Mapa(AgroBase.Models.Operacoes.OperacaoParametrosDadosTrajetoriaModel Trajetoria)
|
||||
{
|
||||
lblMapaInfo_Debug.Content = $"" +
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -13,7 +13,8 @@ arquivo que só deve existir ao final da linha de calibração.
|
|||
Responsabilidades:
|
||||
- selecionar e descobrir o dispositivo DepthAI;
|
||||
- validar CAM_A RGB + CAM_B RE + CAM_C NIR;
|
||||
- resolver/confirmar Bayer da RGB usando preview RAW10;
|
||||
- resolver/confirmar Bayer da RGB usando o RawProcessorPreview oficial;
|
||||
- confirmar a orientação física/canônica das três câmeras;
|
||||
- registrar o domínio de decode RGB escolhido;
|
||||
- criar a pasta oficial do perfil (ex.: calibration/mp_ar0234);
|
||||
- declarar caminhos oficiais para todas as calibrações seguintes;
|
||||
|
|
@ -32,6 +33,11 @@ Exemplos:
|
|||
|
||||
Teclas no preview:
|
||||
B alterna Bayer RGB
|
||||
1/2/3 seleciona RGB/RE/NIR
|
||||
R gira a câmera selecionada 90 graus no sentido horário
|
||||
H alterna flip horizontal da câmera selecionada
|
||||
V alterna flip vertical da câmera selecionada
|
||||
D restaura orientação padrão da câmera selecionada
|
||||
S salva snapshot
|
||||
ENTER confirma e promove o profile
|
||||
Q/ESC cancela sem alterar o profile ativo
|
||||
|
|
@ -55,6 +61,8 @@ import cv2
|
|||
import depthai as dai
|
||||
import numpy as np
|
||||
|
||||
from core.raw_processor_preview import RawProcessorPreview
|
||||
|
||||
|
||||
SCHEMA = "multispec_module_profile_v1"
|
||||
SELECTOR_SCHEMA = "multispec_active_module_profile_v1"
|
||||
|
|
@ -256,6 +264,8 @@ class CameraSpec:
|
|||
resolution_name: str
|
||||
is_color: bool
|
||||
preview_rotate_deg: int
|
||||
preview_flip_horizontal: bool
|
||||
preview_flip_vertical: bool
|
||||
stream_name: str
|
||||
|
||||
|
||||
|
|
@ -302,6 +312,8 @@ def validate_and_build_specs(rows: list[dict]) -> Dict[str, CameraSpec]:
|
|||
resolution_name=str(cfg["resolution_enum"]),
|
||||
is_color=(role == "rgb"),
|
||||
preview_rotate_deg=int(cfg["preview_rotate_deg"]),
|
||||
preview_flip_horizontal=False,
|
||||
preview_flip_vertical=False,
|
||||
stream_name=f"profile_{role}",
|
||||
)
|
||||
|
||||
|
|
@ -364,77 +376,12 @@ def build_preview_pipeline(specs: Dict[str, CameraSpec], fps: float):
|
|||
return pipeline
|
||||
|
||||
|
||||
def unpack_raw10(data, width: int, height: int, stride: Optional[int] = None):
|
||||
width = int(width)
|
||||
height = int(height)
|
||||
if width <= 0 or height <= 0 or width % 4:
|
||||
raise ValueError(f"Dimensão RAW10 inválida: {width}x{height}")
|
||||
|
||||
raw = np.asarray(data, dtype=np.uint8).reshape(-1)
|
||||
payload_bytes = (width // 4) * 5
|
||||
|
||||
candidates = []
|
||||
if stride is not None:
|
||||
try:
|
||||
candidates.append(int(stride))
|
||||
except Exception:
|
||||
pass
|
||||
if height > 0 and raw.size % height == 0:
|
||||
candidates.append(raw.size // height)
|
||||
candidates.append(payload_bytes)
|
||||
|
||||
row_stride = next(
|
||||
(x for x in candidates if x >= payload_bytes and x * height <= raw.size),
|
||||
None,
|
||||
)
|
||||
if row_stride is None:
|
||||
raise ValueError(
|
||||
f"RAW10 curto: bytes={raw.size}, payload mínimo={payload_bytes*height}"
|
||||
)
|
||||
|
||||
rows = raw[: row_stride * height].reshape(height, row_stride)
|
||||
groups = rows[:, :payload_bytes].reshape(height, width // 4, 5)
|
||||
out = np.empty((height, width // 4, 4), dtype=np.uint16)
|
||||
|
||||
out[:, :, 0] = (
|
||||
groups[:, :, 0].astype(np.uint16) << 2
|
||||
) | (groups[:, :, 4] & 0x03)
|
||||
out[:, :, 1] = (
|
||||
groups[:, :, 1].astype(np.uint16) << 2
|
||||
) | ((groups[:, :, 4] >> 2) & 0x03)
|
||||
out[:, :, 2] = (
|
||||
groups[:, :, 2].astype(np.uint16) << 2
|
||||
) | ((groups[:, :, 4] >> 4) & 0x03)
|
||||
out[:, :, 3] = (
|
||||
groups[:, :, 3].astype(np.uint16) << 2
|
||||
) | ((groups[:, :, 4] >> 6) & 0x03)
|
||||
|
||||
return np.ascontiguousarray(out.reshape(height, width))
|
||||
|
||||
|
||||
def bayer_code(pattern: str, algorithm: str):
|
||||
pattern = str(pattern).upper()
|
||||
algorithm = str(algorithm).lower()
|
||||
|
||||
if algorithm == "ea":
|
||||
table = {
|
||||
"BGGR": cv2.COLOR_BayerRG2RGB_EA,
|
||||
"RGGB": cv2.COLOR_BayerBG2RGB_EA,
|
||||
"GRBG": cv2.COLOR_BayerGR2RGB_EA,
|
||||
"GBRG": cv2.COLOR_BayerGB2RGB_EA,
|
||||
}
|
||||
else:
|
||||
table = {
|
||||
"BGGR": cv2.COLOR_BayerRG2RGB,
|
||||
"RGGB": cv2.COLOR_BayerBG2RGB,
|
||||
"GRBG": cv2.COLOR_BayerGR2RGB,
|
||||
"GBRG": cv2.COLOR_BayerGB2RGB,
|
||||
}
|
||||
|
||||
return table[pattern]
|
||||
|
||||
|
||||
def rgb_raw_preview(pkt, spec: CameraSpec, pattern: str, algorithm: str):
|
||||
def rgb_raw_preview(
|
||||
pkt,
|
||||
spec: CameraSpec,
|
||||
processor: RawProcessorPreview,
|
||||
):
|
||||
"""Preview RGB oficial; não mantém um segundo mini-ISP neste script."""
|
||||
raw_type = str(pkt.getType()).upper()
|
||||
if "PACK10" not in raw_type and "RAW10" not in raw_type:
|
||||
raise RuntimeError(
|
||||
|
|
@ -454,31 +401,50 @@ def rgb_raw_preview(pkt, spec: CameraSpec, pattern: str, algorithm: str):
|
|||
except Exception:
|
||||
stride = None
|
||||
|
||||
raw16 = unpack_raw10(pkt.getData(), width, height, stride)
|
||||
rgb16 = cv2.cvtColor(raw16, bayer_code(pattern, algorithm))
|
||||
rgb = np.clip(rgb16.astype(np.float32) / 1023.0, 0.0, 1.0)
|
||||
packed_data = np.asarray(pkt.getData(), dtype=np.uint8).reshape(-1)
|
||||
payload_per_row = (width // 4) * 5
|
||||
if (
|
||||
stride is None
|
||||
or stride < payload_per_row
|
||||
or stride * height > packed_data.size
|
||||
):
|
||||
# Algumas versões do DepthAI reportam getStride() no domínio dos
|
||||
# pixels, não dos bytes PACK10. Nesse caso o tamanho real vence.
|
||||
stride = None
|
||||
|
||||
# Stretch comum aos canais: melhora a inspeção sem alterar o balanço RGB.
|
||||
sample = rgb[::8, ::8]
|
||||
lo = float(np.percentile(sample, 0.5))
|
||||
hi = float(np.percentile(sample, 99.5))
|
||||
if hi <= lo + 1e-6:
|
||||
hi = lo + 1e-6
|
||||
rgb = np.clip((rgb - lo) / (hi - lo), 0.0, 1.0)
|
||||
|
||||
# OpenCV exibe BGR.
|
||||
return np.ascontiguousarray((rgb[:, :, ::-1] * 255.0).astype(np.uint8))
|
||||
raw16 = processor.unpack_raw10_packed(
|
||||
packed_data,
|
||||
stride=stride,
|
||||
)
|
||||
return processor.raw16_to_preview_bgr(raw16)
|
||||
|
||||
|
||||
def apply_preview_orientation(img: np.ndarray, rotate_deg: int):
|
||||
rotate_deg = int(rotate_deg) % 360
|
||||
def normalize_orientation(cfg: dict) -> dict:
|
||||
rotate_deg = int((cfg or {}).get("rotate_deg", 0)) % 360
|
||||
if rotate_deg not in (0, 90, 180, 270):
|
||||
raise ValueError(f"Rotação inválida: {rotate_deg}")
|
||||
return {
|
||||
"rotate_deg": rotate_deg,
|
||||
"flip_horizontal": bool((cfg or {}).get("flip_horizontal", False)),
|
||||
"flip_vertical": bool((cfg or {}).get("flip_vertical", False)),
|
||||
"source": str((cfg or {}).get("source") or "unknown"),
|
||||
}
|
||||
|
||||
|
||||
def apply_preview_orientation(img: np.ndarray, cfg: dict):
|
||||
cfg = normalize_orientation(cfg)
|
||||
rotate_deg = cfg["rotate_deg"]
|
||||
if rotate_deg == 90:
|
||||
return cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE)
|
||||
if rotate_deg == 180:
|
||||
return cv2.rotate(img, cv2.ROTATE_180)
|
||||
if rotate_deg == 270:
|
||||
return cv2.rotate(img, cv2.ROTATE_90_COUNTERCLOCKWISE)
|
||||
return img
|
||||
img = cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE)
|
||||
elif rotate_deg == 180:
|
||||
img = cv2.rotate(img, cv2.ROTATE_180)
|
||||
elif rotate_deg == 270:
|
||||
img = cv2.rotate(img, cv2.ROTATE_90_COUNTERCLOCKWISE)
|
||||
if cfg["flip_horizontal"]:
|
||||
img = cv2.flip(img, 1)
|
||||
if cfg["flip_vertical"]:
|
||||
img = cv2.flip(img, 0)
|
||||
return np.ascontiguousarray(img)
|
||||
|
||||
|
||||
def overlay(img, lines, x=12, y=24, scale=0.48, step=21):
|
||||
|
|
@ -499,6 +465,7 @@ def preview_and_confirm(
|
|||
dev_info,
|
||||
specs: Dict[str, CameraSpec],
|
||||
initial_bayer: str,
|
||||
initial_orientation: Dict[str, dict],
|
||||
algorithm: str,
|
||||
fps: float,
|
||||
panel_width: int,
|
||||
|
|
@ -506,8 +473,25 @@ def preview_and_confirm(
|
|||
snapshots_dir: Path,
|
||||
):
|
||||
pattern = initial_bayer
|
||||
orientation = {
|
||||
role: normalize_orientation(initial_orientation[role])
|
||||
for role in ROLES
|
||||
}
|
||||
orientation_defaults = {
|
||||
role: dict(orientation[role])
|
||||
for role in ROLES
|
||||
}
|
||||
selected_role = "rgb"
|
||||
preview_processors = {
|
||||
candidate: RawProcessorPreview(
|
||||
sensor_width=specs["rgb"].width,
|
||||
sensor_height=specs["rgb"].height,
|
||||
bayer_pattern=candidate,
|
||||
)
|
||||
for candidate in BAYER_PATTERNS
|
||||
}
|
||||
pipeline = build_preview_pipeline(specs, fps)
|
||||
window = "Module Profile - Bayer Confirmation"
|
||||
window = "Module Profile - Physical Sanity Check"
|
||||
cv2.namedWindow(window, cv2.WINDOW_NORMAL)
|
||||
|
||||
try:
|
||||
|
|
@ -538,7 +522,11 @@ def preview_and_confirm(
|
|||
spec = specs[role]
|
||||
pkt = latest[role]
|
||||
if role == "rgb":
|
||||
view = rgb_raw_preview(pkt, spec, pattern, algorithm)
|
||||
view = rgb_raw_preview(
|
||||
pkt,
|
||||
spec,
|
||||
preview_processors[pattern],
|
||||
)
|
||||
else:
|
||||
view = pkt.getCvFrame()
|
||||
if view.ndim == 2:
|
||||
|
|
@ -546,7 +534,7 @@ def preview_and_confirm(
|
|||
|
||||
view = apply_preview_orientation(
|
||||
view,
|
||||
spec.preview_rotate_deg,
|
||||
orientation[role],
|
||||
)
|
||||
panel = cv2.resize(
|
||||
view,
|
||||
|
|
@ -554,16 +542,33 @@ def preview_and_confirm(
|
|||
interpolation=cv2.INTER_AREA,
|
||||
)
|
||||
lines = [
|
||||
f"{role.upper()} | {spec.socket} | {spec.sensor}",
|
||||
(
|
||||
f"> {role.upper()} | {spec.socket} | {spec.sensor}"
|
||||
if role == selected_role
|
||||
else f" {role.upper()} | {spec.socket} | {spec.sensor}"
|
||||
),
|
||||
f"native={spec.width}x{spec.height}",
|
||||
f"packet={latest_type[role]}",
|
||||
(
|
||||
f"ROT={orientation[role]['rotate_deg']} "
|
||||
f"FH={int(orientation[role]['flip_horizontal'])} "
|
||||
f"FV={int(orientation[role]['flip_vertical'])}"
|
||||
),
|
||||
]
|
||||
if role == "rgb":
|
||||
lines += [
|
||||
f"BAYER={pattern} | DEMOSAIC={algorithm}",
|
||||
f"BAYER={pattern} | PREVIEW=RawProcessorPreview",
|
||||
"Confira vermelho/azul em objeto conhecido",
|
||||
]
|
||||
overlay(panel, lines)
|
||||
if role == selected_role:
|
||||
cv2.rectangle(
|
||||
panel,
|
||||
(2, 2),
|
||||
(panel_width - 3, panel_height - 3),
|
||||
(0, 255, 255),
|
||||
4,
|
||||
)
|
||||
panels.append(panel)
|
||||
|
||||
info = np.zeros(
|
||||
|
|
@ -575,14 +580,19 @@ def preview_and_confirm(
|
|||
"",
|
||||
f"RGB Bayer selecionado: {pattern}",
|
||||
f"RGB processing: linear_demosaic / {algorithm}",
|
||||
f"Camera selecionada: {selected_role.upper()}",
|
||||
"",
|
||||
"B = alternar Bayer",
|
||||
"1/2/3 = selecionar RGB/RE/NIR",
|
||||
"R = girar 90 graus horario",
|
||||
"H/V = flip horizontal/vertical",
|
||||
"D = restaurar orientacao padrao",
|
||||
"S = snapshot",
|
||||
"ENTER = confirmar e promover",
|
||||
"Q/ESC = cancelar",
|
||||
"",
|
||||
"Use um objeto vermelho e outro azul",
|
||||
"para confirmar que R/B não estão trocados.",
|
||||
"Confirme cores e o mesmo sentido fisico",
|
||||
"nas tres cameras antes de pressionar ENTER.",
|
||||
])
|
||||
|
||||
last_board = np.vstack([
|
||||
|
|
@ -597,21 +607,48 @@ def preview_and_confirm(
|
|||
|
||||
key = cv2.waitKey(1) & 0xFF
|
||||
if key in (ord("q"), ord("Q"), 27):
|
||||
raise KeyboardInterrupt("Cancelado durante confirmação do Bayer.")
|
||||
raise KeyboardInterrupt("Cancelado durante sanity check físico.")
|
||||
if key == ord("1"):
|
||||
selected_role = "rgb"
|
||||
elif key == ord("2"):
|
||||
selected_role = "re"
|
||||
elif key == ord("3"):
|
||||
selected_role = "nir"
|
||||
if key in (ord("b"), ord("B")):
|
||||
idx = (BAYER_PATTERNS.index(pattern) + 1) % len(BAYER_PATTERNS)
|
||||
pattern = BAYER_PATTERNS[idx]
|
||||
if key in (ord("r"), ord("R")):
|
||||
orientation[selected_role]["rotate_deg"] = (
|
||||
int(orientation[selected_role]["rotate_deg"]) + 90
|
||||
) % 360
|
||||
orientation[selected_role]["source"] = "raw_preview_confirmed"
|
||||
if key in (ord("h"), ord("H")):
|
||||
orientation[selected_role]["flip_horizontal"] = not bool(
|
||||
orientation[selected_role]["flip_horizontal"]
|
||||
)
|
||||
orientation[selected_role]["source"] = "raw_preview_confirmed"
|
||||
if key in (ord("v"), ord("V")):
|
||||
orientation[selected_role]["flip_vertical"] = not bool(
|
||||
orientation[selected_role]["flip_vertical"]
|
||||
)
|
||||
orientation[selected_role]["source"] = "raw_preview_confirmed"
|
||||
if key in (ord("d"), ord("D")):
|
||||
orientation[selected_role] = dict(
|
||||
orientation_defaults[selected_role]
|
||||
)
|
||||
if key in (ord("s"), ord("S")) and last_board is not None:
|
||||
ensure_dir(snapshots_dir)
|
||||
cv2.imwrite(
|
||||
str(snapshots_dir / f"bayer_{pattern}_{stamp()}.png"),
|
||||
str(snapshots_dir / f"physical_check_{pattern}_{stamp()}.png"),
|
||||
last_board,
|
||||
)
|
||||
if key in (10, 13) and last_board is not None:
|
||||
ensure_dir(snapshots_dir)
|
||||
confirmed_path = snapshots_dir / "bayer_confirmed.png"
|
||||
for role in ROLES:
|
||||
orientation[role]["source"] = "raw_preview_confirmed"
|
||||
confirmed_path = snapshots_dir / "physical_sanity_confirmed.png"
|
||||
cv2.imwrite(str(confirmed_path), last_board)
|
||||
return pattern, confirmed_path
|
||||
return pattern, orientation, confirmed_path
|
||||
|
||||
time.sleep(0.002)
|
||||
finally:
|
||||
|
|
@ -625,9 +662,9 @@ def artifact_paths(profile_dir: Path) -> dict:
|
|||
"intrinsics_json": norm_path(profile_dir / "intrinsics_calibration_v1.json"),
|
||||
"flatfield_npz": norm_path(profile_dir / "flatfield_maps_v1.npz"),
|
||||
"flatfield_json": norm_path(profile_dir / "flatfield_maps_v1.json"),
|
||||
"radiometry_json": norm_path(profile_dir / "radiometry_calibration_v1.json"),
|
||||
"camera_startup_json": norm_path(profile_dir / "camera_startup_profile_v1.json"),
|
||||
"homography_json": norm_path(profile_dir / "homography_calibration_v1.json"),
|
||||
"radiometry_json": norm_path(profile_dir / "radiometry_calibration_v5.json"),
|
||||
"camera_startup_json": norm_path(profile_dir / "camera_startup_profile_v3.json"),
|
||||
"homography_json": norm_path(profile_dir / "homography_calibration_v4.json"),
|
||||
"module_params_json": norm_path(profile_dir / "module_params.json"),
|
||||
"module_params_assembly_report_json": norm_path(
|
||||
profile_dir / "module_params_assembly_report.json"
|
||||
|
|
@ -652,19 +689,27 @@ def build_profile(
|
|||
profile_name: str,
|
||||
profile_dir: Path,
|
||||
bayer: str,
|
||||
preview_orientation: Dict[str, dict],
|
||||
confirmed_with_raw_preview: bool,
|
||||
confirmation_snapshot: Optional[Path],
|
||||
):
|
||||
orientation = {
|
||||
role: {
|
||||
"rotate_deg": int(specs[role].preview_rotate_deg),
|
||||
"flip_horizontal": False,
|
||||
"flip_vertical": False,
|
||||
"source": "sensor_default",
|
||||
}
|
||||
role: normalize_orientation(preview_orientation[role])
|
||||
for role in ROLES
|
||||
}
|
||||
|
||||
resolved_setup = {}
|
||||
for role in ROLES:
|
||||
item = asdict(specs[role])
|
||||
item["preview_rotate_deg"] = int(orientation[role]["rotate_deg"])
|
||||
item["preview_flip_horizontal"] = bool(
|
||||
orientation[role]["flip_horizontal"]
|
||||
)
|
||||
item["preview_flip_vertical"] = bool(
|
||||
orientation[role]["flip_vertical"]
|
||||
)
|
||||
resolved_setup[role] = item
|
||||
|
||||
hardware_signature = {
|
||||
role: {
|
||||
"socket": specs[role].socket,
|
||||
|
|
@ -686,10 +731,7 @@ def build_profile(
|
|||
"depthai_version": getattr(dai, "__version__", "unknown"),
|
||||
"usb_speed": usb_speed,
|
||||
"hardware_signature": hardware_signature,
|
||||
"resolved_setup": {
|
||||
role: asdict(specs[role])
|
||||
for role in ROLES
|
||||
},
|
||||
"resolved_setup": resolved_setup,
|
||||
"camera_inventory": rows,
|
||||
"rgb_decode": {
|
||||
"bayer_pattern": bayer,
|
||||
|
|
@ -708,9 +750,23 @@ def build_profile(
|
|||
),
|
||||
},
|
||||
"preview_orientation_defaults": orientation,
|
||||
"mounting_sanity_check": {
|
||||
"confirmed_with_live_preview": bool(confirmed_with_raw_preview),
|
||||
"preview_engine": "core.raw_processor_preview.RawProcessorPreview",
|
||||
"orientation_by_role": orientation,
|
||||
"confirmation_snapshot": (
|
||||
norm_path(confirmation_snapshot)
|
||||
if confirmation_snapshot is not None
|
||||
else None
|
||||
),
|
||||
},
|
||||
"geometry_bootstrap": {
|
||||
"native_space": "native_stream_no_external_undistort",
|
||||
"orientation_status": "preview_default_until_homography",
|
||||
"orientation_status": (
|
||||
"raw_preview_confirmed_until_homography"
|
||||
if confirmed_with_raw_preview
|
||||
else "default_or_cli_headless_until_homography"
|
||||
),
|
||||
"note": (
|
||||
"A Homography homologada será a autoridade final da orientação "
|
||||
"científica e do espaço canônico."
|
||||
|
|
@ -730,6 +786,15 @@ def build_profile(
|
|||
"mode": args.rgb_processing_mode,
|
||||
"demosaic_algorithm": args.demosaic_algorithm,
|
||||
},
|
||||
"camera_orientation_bootstrap": {
|
||||
"enabled": True,
|
||||
"input_space": "native_stream_no_external_undistort",
|
||||
"output_space": (
|
||||
"canonical_oriented_stream_no_external_undistort"
|
||||
),
|
||||
"by_role": orientation,
|
||||
"authority": "module_profile_until_homography",
|
||||
},
|
||||
},
|
||||
"artifact_paths": artifact_paths(profile_dir),
|
||||
"traceability": {
|
||||
|
|
@ -740,6 +805,13 @@ def build_profile(
|
|||
},
|
||||
"notes": args.notes or "",
|
||||
},
|
||||
"capture_synchronization": {
|
||||
"hardware_sync_enabled": bool(args.hardware_sync_enabled),
|
||||
"frame_sync_master": args.frame_sync_master,
|
||||
"software_sync_mode": "best",
|
||||
"sync_tolerance_ms": 12.0,
|
||||
"buffer_size": 8,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -789,6 +861,29 @@ def backup_if_exists(path: Path):
|
|||
return None
|
||||
|
||||
|
||||
def resolve_initial_orientation(
|
||||
specs: Dict[str, CameraSpec],
|
||||
args,
|
||||
) -> Dict[str, dict]:
|
||||
result = {}
|
||||
for role in ROLES:
|
||||
rotate_override = getattr(args, f"{role}_preview_rotate_deg")
|
||||
flip_h = bool(getattr(args, f"{role}_preview_flip_horizontal"))
|
||||
flip_v = bool(getattr(args, f"{role}_preview_flip_vertical"))
|
||||
has_cli_override = rotate_override is not None or flip_h or flip_v
|
||||
result[role] = normalize_orientation({
|
||||
"rotate_deg": (
|
||||
specs[role].preview_rotate_deg
|
||||
if rotate_override is None
|
||||
else rotate_override
|
||||
),
|
||||
"flip_horizontal": flip_h,
|
||||
"flip_vertical": flip_v,
|
||||
"source": "cli" if has_cli_override else "sensor_default",
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Bootstrap do profile físico/decode do módulo multiespectral.",
|
||||
|
|
@ -826,6 +921,25 @@ def main():
|
|||
parser.add_argument("--fps", type=float, default=10.0)
|
||||
parser.add_argument("--panel-width", type=int, default=640)
|
||||
parser.add_argument("--panel-height", type=int, default=400)
|
||||
for role in ROLES:
|
||||
parser.add_argument(
|
||||
f"--{role}-preview-rotate-deg",
|
||||
type=int,
|
||||
default=None,
|
||||
choices=[0, 90, 180, 270],
|
||||
help=(
|
||||
f"Orientação inicial de {role.upper()}; pode ser ajustada "
|
||||
"interativamente no preview."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
f"--{role}-preview-flip-horizontal",
|
||||
action="store_true",
|
||||
)
|
||||
parser.add_argument(
|
||||
f"--{role}-preview-flip-vertical",
|
||||
action="store_true",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-preview",
|
||||
action="store_true",
|
||||
|
|
@ -841,6 +955,16 @@ def main():
|
|||
parser.add_argument("--lens-re", default="")
|
||||
parser.add_argument("--lens-nir", default="")
|
||||
parser.add_argument("--notes", default="")
|
||||
parser.add_argument(
|
||||
"--hardware-sync-enabled",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--frame-sync-master",
|
||||
default="CAM_A",
|
||||
choices=["CAM_A", "CAM_B", "CAM_C"],
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.no_preview and not args.yes:
|
||||
|
|
@ -849,6 +973,7 @@ def main():
|
|||
dev_info = select_device_info(args.mx_id)
|
||||
rows, actual_mx, usb_speed = discover(dev_info)
|
||||
specs = validate_and_build_specs(rows)
|
||||
selected_orientation = resolve_initial_orientation(specs, args)
|
||||
|
||||
rgb_sensor = specs["rgb"].sensor
|
||||
default_profile_name = f"mp_{rgb_sensor.lower()}"
|
||||
|
|
@ -898,6 +1023,13 @@ def main():
|
|||
f"RGB decode : Bayer={selected_bayer} ({bayer_initial_source}) | "
|
||||
f"mode={args.rgb_processing_mode} | algo={args.demosaic_algorithm}"
|
||||
)
|
||||
for role in ROLES:
|
||||
orient = selected_orientation[role]
|
||||
print(
|
||||
f"{role.upper():3s} orient. : rotate={orient['rotate_deg']:3d} | "
|
||||
f"flip_h={orient['flip_horizontal']} | "
|
||||
f"flip_v={orient['flip_vertical']} | source={orient['source']}"
|
||||
)
|
||||
print("=" * 88)
|
||||
|
||||
confirmed_with_raw_preview = False
|
||||
|
|
@ -905,10 +1037,15 @@ def main():
|
|||
|
||||
try:
|
||||
if not args.no_preview:
|
||||
selected_bayer, confirmation_snapshot = preview_and_confirm(
|
||||
(
|
||||
selected_bayer,
|
||||
selected_orientation,
|
||||
confirmation_snapshot,
|
||||
) = preview_and_confirm(
|
||||
dev_info=dev_info,
|
||||
specs=specs,
|
||||
initial_bayer=selected_bayer,
|
||||
initial_orientation=selected_orientation,
|
||||
algorithm=args.demosaic_algorithm,
|
||||
fps=args.fps,
|
||||
panel_width=args.panel_width,
|
||||
|
|
@ -926,6 +1063,7 @@ def main():
|
|||
profile_name=profile_name,
|
||||
profile_dir=profile_dir,
|
||||
bayer=selected_bayer,
|
||||
preview_orientation=selected_orientation,
|
||||
confirmed_with_raw_preview=confirmed_with_raw_preview,
|
||||
confirmation_snapshot=confirmation_snapshot,
|
||||
)
|
||||
|
|
@ -965,6 +1103,14 @@ def main():
|
|||
print(f"[SELECTOR] {active_selector}")
|
||||
print(f"[SHA256] {selector['profile_sha256']}")
|
||||
print(f"[BAYER] {selected_bayer}")
|
||||
for role in ROLES:
|
||||
orient = selected_orientation[role]
|
||||
print(
|
||||
f"[ORIENTATION] {role.upper():3s} | "
|
||||
f"rotate={orient['rotate_deg']} | "
|
||||
f"flip_h={orient['flip_horizontal']} | "
|
||||
f"flip_v={orient['flip_vertical']}"
|
||||
)
|
||||
if backup_profile:
|
||||
print(f"[BACKUP PROFILE] {backup_profile}")
|
||||
if backup_selector:
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ from __future__ import annotations
|
|||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import shutil
|
||||
import zipfile
|
||||
|
|
@ -536,6 +537,78 @@ def resolve_output_path(value: str | Path) -> Path:
|
|||
return Path(value).expanduser()
|
||||
|
||||
|
||||
def normalize_capture_synchronization(
|
||||
value: dict,
|
||||
hardware_signature: dict,
|
||||
) -> dict:
|
||||
"""Valida e normaliza o contrato de sincronismo vindo do Module Profile."""
|
||||
if not isinstance(value, dict):
|
||||
raise RuntimeError(
|
||||
"Module Profile sem capture_synchronization. "
|
||||
"Execute novamente o Script 0 revisado."
|
||||
)
|
||||
|
||||
enabled = value.get("hardware_sync_enabled")
|
||||
if not isinstance(enabled, bool):
|
||||
raise RuntimeError(
|
||||
"capture_synchronization.hardware_sync_enabled deve ser bool."
|
||||
)
|
||||
|
||||
master = str(value.get("frame_sync_master") or "").upper().strip()
|
||||
valid_sockets = {
|
||||
str(item["socket"]).upper()
|
||||
for item in normalize_signature(hardware_signature).values()
|
||||
}
|
||||
if master not in valid_sockets:
|
||||
raise RuntimeError(
|
||||
"capture_synchronization.frame_sync_master inválido: "
|
||||
f"{master!r}. Disponíveis={sorted(valid_sockets)}"
|
||||
)
|
||||
|
||||
software_mode = str(
|
||||
value.get("software_sync_mode") or ""
|
||||
).lower().strip()
|
||||
if not software_mode:
|
||||
raise RuntimeError(
|
||||
"capture_synchronization.software_sync_mode ausente."
|
||||
)
|
||||
|
||||
try:
|
||||
tolerance_ms = float(value.get("sync_tolerance_ms"))
|
||||
except Exception as exc:
|
||||
raise RuntimeError(
|
||||
"capture_synchronization.sync_tolerance_ms inválido."
|
||||
) from exc
|
||||
if not math.isfinite(tolerance_ms) or tolerance_ms <= 0.0:
|
||||
raise RuntimeError(
|
||||
"capture_synchronization.sync_tolerance_ms deve ser finito e > 0."
|
||||
)
|
||||
|
||||
buffer_size = value.get("buffer_size")
|
||||
if isinstance(buffer_size, bool):
|
||||
raise RuntimeError(
|
||||
"capture_synchronization.buffer_size deve ser inteiro positivo."
|
||||
)
|
||||
try:
|
||||
buffer_size = int(buffer_size)
|
||||
except Exception as exc:
|
||||
raise RuntimeError(
|
||||
"capture_synchronization.buffer_size inválido."
|
||||
) from exc
|
||||
if buffer_size <= 0:
|
||||
raise RuntimeError(
|
||||
"capture_synchronization.buffer_size deve ser > 0."
|
||||
)
|
||||
|
||||
return {
|
||||
"hardware_sync_enabled": enabled,
|
||||
"frame_sync_master": master,
|
||||
"software_sync_mode": software_mode,
|
||||
"sync_tolerance_ms": tolerance_ms,
|
||||
"buffer_size": buffer_size,
|
||||
}
|
||||
|
||||
|
||||
def validate_module_profile(profile: dict, profile_path: Path):
|
||||
if profile.get("schema") != MODULE_PROFILE_SCHEMA:
|
||||
raise RuntimeError(
|
||||
|
|
@ -602,6 +675,11 @@ def validate_module_profile(profile: dict, profile_path: Path):
|
|||
if not artifacts.get(key):
|
||||
raise RuntimeError(f"Module Profile sem artifact_paths.{key}.")
|
||||
|
||||
normalize_capture_synchronization(
|
||||
profile.get("capture_synchronization"),
|
||||
signature,
|
||||
)
|
||||
|
||||
|
||||
def load_module_profile_contract(
|
||||
module_profile_arg: Optional[str],
|
||||
|
|
@ -2602,6 +2680,10 @@ def runtime_policy_from_module_profile(profile: dict) -> dict:
|
|||
sensor nem herança de um module_params anterior.
|
||||
"""
|
||||
decode = profile["rgb_decode"]
|
||||
capture_sync = normalize_capture_synchronization(
|
||||
profile.get("capture_synchronization"),
|
||||
profile.get("hardware_signature"),
|
||||
)
|
||||
declared = profile.get("runtime_policy")
|
||||
if declared is None:
|
||||
declared = {}
|
||||
|
|
@ -2614,6 +2696,7 @@ def runtime_policy_from_module_profile(profile: dict) -> dict:
|
|||
"capture_mode_requested": "AUTO",
|
||||
"capture_mode_effective": "AUTO",
|
||||
"raw_policy": "allow_single",
|
||||
"capture_synchronization": capture_sync,
|
||||
"rgb_processing": {
|
||||
"mode": str(decode["mode"]).lower(),
|
||||
"demosaic_algorithm": str(decode["demosaic_algorithm"]).lower(),
|
||||
|
|
@ -2781,6 +2864,9 @@ def build_final_module_params(
|
|||
"raw_policy": runtime_policy[
|
||||
"raw_policy"
|
||||
],
|
||||
"capture_synchronization": deepcopy(
|
||||
runtime_policy["capture_synchronization"]
|
||||
),
|
||||
|
||||
# Compatibilidade root: RGB reference camera
|
||||
"sensor_width": int(
|
||||
|
|
@ -2958,6 +3044,7 @@ def validate_final_module_params(
|
|||
"bayer_pattern",
|
||||
"sensor_size_by_role",
|
||||
"camera_hardware",
|
||||
"capture_synchronization",
|
||||
"camera_orientation",
|
||||
"rgb_processing",
|
||||
"camera_settings",
|
||||
|
|
@ -3013,6 +3100,15 @@ def validate_final_module_params(
|
|||
|
||||
if str(mp.get("bayer_pattern") or "").upper() not in BAYER_PATTERNS:
|
||||
raise RuntimeError("bayer_pattern final inválido.")
|
||||
|
||||
normalized_sync = normalize_capture_synchronization(
|
||||
mp.get("capture_synchronization"),
|
||||
mp.get("camera_hardware"),
|
||||
)
|
||||
if mp.get("capture_synchronization") != normalized_sync:
|
||||
raise RuntimeError(
|
||||
"capture_synchronization final não está normalizado."
|
||||
)
|
||||
rgb_processing = mp.get("rgb_processing")
|
||||
if not isinstance(rgb_processing, dict):
|
||||
raise RuntimeError("rgb_processing final inválido.")
|
||||
|
|
@ -3698,6 +3794,9 @@ def main():
|
|||
provenance = {
|
||||
"module_profile_contract": module_profile_provenance(profile_contract),
|
||||
"configuration_source": "module_profile_only",
|
||||
"capture_synchronization": deepcopy(
|
||||
runtime_policy["capture_synchronization"]
|
||||
),
|
||||
"hardware_signature": deepcopy(
|
||||
common_signature
|
||||
),
|
||||
|
|
@ -3832,6 +3931,9 @@ def main():
|
|||
"camera_orientation": deepcopy(
|
||||
camera_orientation
|
||||
),
|
||||
"capture_synchronization": deepcopy(
|
||||
runtime_policy["capture_synchronization"]
|
||||
),
|
||||
"runtime_policy": runtime_policy,
|
||||
"calibration_provenance": provenance,
|
||||
"final_checks": {
|
||||
|
|
@ -3847,6 +3949,7 @@ def main():
|
|||
"hardware_consistency": "pass",
|
||||
"geometry_consistency": "pass",
|
||||
"camera_orientation": "pass",
|
||||
"capture_synchronization": "pass",
|
||||
"orientation_bootstrap_consistency": "pass",
|
||||
"startup_radiometry_consistency": "pass",
|
||||
"flatfield_npz_integrity": "pass",
|
||||
|
|
@ -3874,6 +3977,15 @@ def main():
|
|||
f"{common_signature['nir']['size'][0]}x{common_signature['nir']['size'][1]}"
|
||||
)
|
||||
print(f"Bayer : {bayer}")
|
||||
capture_sync = runtime_policy["capture_synchronization"]
|
||||
print(
|
||||
"Capture sync: "
|
||||
f"hardware={'ON' if capture_sync['hardware_sync_enabled'] else 'OFF'} | "
|
||||
f"master={capture_sync['frame_sync_master']} | "
|
||||
f"software={capture_sync['software_sync_mode']} | "
|
||||
f"tolerance={capture_sync['sync_tolerance_ms']:.1f}ms | "
|
||||
f"buffer={capture_sync['buffer_size']}"
|
||||
)
|
||||
print(
|
||||
f"RGB process : "
|
||||
f"{runtime_policy['rgb_processing'].get('mode')}"
|
||||
|
|
|
|||
|
|
@ -41,12 +41,17 @@ class OakFcc3Client:
|
|||
capture_mode="AUTO",
|
||||
raw_policy="allow_single",
|
||||
module_calibration_json=None,
|
||||
sync_mode="best",
|
||||
sync_tolerance_ms=25.0,
|
||||
mx_id=None,
|
||||
imu_modo="rotation_vector",
|
||||
imu_freq_hz=200,
|
||||
evaluate_quality=True,
|
||||
|
||||
hardware_sync_enabled=None,
|
||||
frame_sync_master=None,
|
||||
sync_mode=None,
|
||||
sync_tolerance_ms=None,
|
||||
buffer_size=None,
|
||||
|
||||
**kwargs,
|
||||
):
|
||||
self.width = width
|
||||
|
|
@ -85,14 +90,18 @@ class OakFcc3Client:
|
|||
output_dtype=output_dtype,
|
||||
capture_mode=capture_mode,
|
||||
raw_policy=raw_policy,
|
||||
sync_mode=sync_mode,
|
||||
sync_tolerance_ms=sync_tolerance_ms,
|
||||
mx_id=self.mx_id,
|
||||
module_calibration_json=module_calibration_json,
|
||||
|
||||
imu_modo=self.imu_modo,
|
||||
imu_freq_hz=self.imu_freq_hz,
|
||||
|
||||
hardware_sync_enabled=hardware_sync_enabled,
|
||||
frame_sync_master=frame_sync_master,
|
||||
sync_mode=sync_mode,
|
||||
sync_tolerance_ms=sync_tolerance_ms,
|
||||
buffer_size=buffer_size,
|
||||
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -44,11 +44,11 @@ class OakFcc3Manager:
|
|||
capture_mode="AUTO",
|
||||
raw_policy="allow_single",
|
||||
roles=None,
|
||||
sync_mode="best",
|
||||
hardware_sync_enabled=True,
|
||||
frame_sync_master="CAM_A",
|
||||
sync_tolerance_ms=12.0,
|
||||
buffer_size=8,
|
||||
sync_mode=None,
|
||||
hardware_sync_enabled=None,
|
||||
frame_sync_master=None,
|
||||
sync_tolerance_ms=None,
|
||||
buffer_size=None,
|
||||
only_camera=None,
|
||||
mx_id=None,
|
||||
module_calibration_json=None,
|
||||
|
|
@ -56,9 +56,6 @@ class OakFcc3Manager:
|
|||
imu_modo="rotation_vector",
|
||||
imu_freq_hz=200,
|
||||
):
|
||||
self.hardware_sync_enabled = hardware_sync_enabled
|
||||
self.frame_sync_master = frame_sync_master
|
||||
|
||||
self.fps = fps
|
||||
|
||||
# Para compatibilidade, mantemos width/height.
|
||||
|
|
@ -83,10 +80,6 @@ class OakFcc3Manager:
|
|||
"CAM_C": "nir",
|
||||
}
|
||||
|
||||
self.sync_mode = sync_mode
|
||||
self.sync_tolerance_ms = sync_tolerance_ms
|
||||
self.buffer_size = buffer_size
|
||||
|
||||
self.mx_id = str(mx_id) if mx_id else None
|
||||
self.dev_info = None
|
||||
self.device = None
|
||||
|
|
@ -127,6 +120,18 @@ class OakFcc3Manager:
|
|||
self.fusion_config = (self.module_params or {}).get("fusion_config", {}) or {}
|
||||
self.aligned_geometry = None
|
||||
|
||||
sync_cfg = (self.module_params.get("capture_synchronization", {}) if isinstance(self.module_params, dict) else {})
|
||||
if hardware_sync_enabled is None: hardware_sync_enabled = sync_cfg.get("hardware_sync_enabled", False)
|
||||
if frame_sync_master is None: frame_sync_master = sync_cfg.get("frame_sync_master", "CAM_A")
|
||||
if sync_mode is None: sync_mode = sync_cfg.get("software_sync_mode", "best")
|
||||
if sync_tolerance_ms is None: sync_tolerance_ms = sync_cfg.get("sync_tolerance_ms", 12.0)
|
||||
if buffer_size is None: buffer_size = sync_cfg.get("buffer_size", 8)
|
||||
self.hardware_sync_enabled = bool(hardware_sync_enabled)
|
||||
self.frame_sync_master = str(frame_sync_master)
|
||||
self.sync_mode = str(sync_mode)
|
||||
self.sync_tolerance_ms = float(sync_tolerance_ms)
|
||||
self.buffer_size = int(buffer_size)
|
||||
|
||||
self.async_capture_enabled = True
|
||||
self.async_capture_mode = "latest" # latest | queue
|
||||
self.async_capture_max_queue = 2
|
||||
|
|
|
|||
|
|
@ -1,126 +1,689 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
RawProcessorPreview
|
||||
===================
|
||||
|
||||
Mini-ISP exclusivamente visual para gerar previews de anotação a partir do
|
||||
RAW Bayer da câmera RGB.
|
||||
|
||||
Contrato:
|
||||
- entrada científica permanece imutável;
|
||||
- saída sempre BGR uint8 para OpenCV;
|
||||
- preserva exatamente HxW do raster RGB;
|
||||
- não aplica resize, crop, rotate/flip, undistort, Flat-Field ou Homography;
|
||||
- não deve ser usado para formar o tensor de treino/inferência.
|
||||
|
||||
Pipeline visual padrão:
|
||||
RAW10/RAW16 -> níveis robustos -> gamma -> demosaico EA/BGR ->
|
||||
WB por regiões claras/neutras -> CLAHE suave em luminância ->
|
||||
saturação leve -> contraste -> unsharp suave.
|
||||
|
||||
As APIs antigas foram preservadas. Chamadas que antes usavam
|
||||
raw16_to_preview_bgr() ou raw16_to_preview_jpg_bytes() continuam válidas.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from typing import Any, Optional
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class RawProcessorPreview:
|
||||
def __init__(self, sensor_width: int, sensor_height: int, bayer_pattern: str = "GBRG"):
|
||||
self.sensor_width = sensor_width
|
||||
self.sensor_height = sensor_height
|
||||
self.bayer_pattern = bayer_pattern.upper()
|
||||
VERSION = "raw_processor_preview_v2_2026_09_09"
|
||||
CONTRACT_SCHEMA = "rgb_raw_annotation_preview_v2"
|
||||
BAYER_PATTERNS = ("RGGB", "BGGR", "GRBG", "GBRG")
|
||||
|
||||
def raw16_to_vis8(
|
||||
self, raw16: np.ndarray,
|
||||
black_level: Optional[int] = None,
|
||||
white_level: Optional[int] = None,
|
||||
gamma: float = 2.2,
|
||||
bit_depth: int = 10
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Conversão para visualização:
|
||||
- auto-level
|
||||
- gamma
|
||||
"""
|
||||
max_val = float((1 << bit_depth) - 1)
|
||||
DEFAULT_CONFIG = {
|
||||
"bit_depth": 10,
|
||||
"levels": {
|
||||
"mode": "robust_percentile",
|
||||
"black_percentile": 0.20,
|
||||
"white_percentile": 99.80,
|
||||
"sample_max_pixels": 500_000,
|
||||
"minimum_span_codes": 16.0,
|
||||
},
|
||||
"gamma": 2.20,
|
||||
"demosaic": {
|
||||
"algorithm": "edge_aware",
|
||||
"output_color_order": "BGR",
|
||||
},
|
||||
"white_balance": {
|
||||
"enabled": True,
|
||||
"method": "neutral_bright_with_gray_world_fallback",
|
||||
"strength": 0.65,
|
||||
"gain_min": 0.60,
|
||||
"gain_max": 1.70,
|
||||
"bright_percentile": 55.0,
|
||||
"neutral_chroma_percentile": 40.0,
|
||||
"min_neutral_pixels": 256,
|
||||
},
|
||||
"clahe": {
|
||||
"enabled": True,
|
||||
"clip_limit": 1.55,
|
||||
"tile_grid_size": 8,
|
||||
},
|
||||
"saturation": {
|
||||
"enabled": True,
|
||||
"factor": 1.04,
|
||||
},
|
||||
"contrast": {
|
||||
"enabled": True,
|
||||
"alpha": 1.04,
|
||||
"beta": 0.0,
|
||||
},
|
||||
"sharpen": {
|
||||
"enabled": True,
|
||||
"sigma": 0.85,
|
||||
"amount": 0.55,
|
||||
"threshold": 2.0,
|
||||
},
|
||||
}
|
||||
|
||||
raw = raw16.astype(np.float32)
|
||||
def __init__(
|
||||
self,
|
||||
sensor_width: int,
|
||||
sensor_height: int,
|
||||
bayer_pattern: str = "GBRG",
|
||||
config: Optional[dict] = None,
|
||||
):
|
||||
self.sensor_width = int(sensor_width)
|
||||
self.sensor_height = int(sensor_height)
|
||||
self.bayer_pattern = str(bayer_pattern).upper()
|
||||
self.config = self._deep_merge(self.DEFAULT_CONFIG, config or {})
|
||||
self.last_preview_report: dict[str, Any] = {}
|
||||
self._validate_contract()
|
||||
|
||||
if black_level is None:
|
||||
black_level = float(raw.min())
|
||||
if white_level is None:
|
||||
white_level = float(raw.max())
|
||||
@staticmethod
|
||||
def _deep_merge(base: dict, override: dict) -> dict:
|
||||
result = deepcopy(base)
|
||||
if not isinstance(override, dict):
|
||||
raise TypeError("config do RawProcessorPreview deve ser dict.")
|
||||
for key, value in override.items():
|
||||
if isinstance(value, dict) and isinstance(result.get(key), dict):
|
||||
result[key] = RawProcessorPreview._deep_merge(result[key], value)
|
||||
else:
|
||||
result[key] = deepcopy(value)
|
||||
return result
|
||||
|
||||
if white_level <= black_level:
|
||||
norm = raw / max_val
|
||||
else:
|
||||
norm = (raw - black_level) / (white_level - black_level)
|
||||
|
||||
norm = np.clip(norm, 0.0, 1.0)
|
||||
|
||||
if gamma is not None and gamma > 0:
|
||||
norm = np.power(norm, 1.0 / gamma)
|
||||
|
||||
return (norm * 255.0).clip(0, 255).astype(np.uint8)
|
||||
|
||||
def _debayer_code(self):
|
||||
mapping = {
|
||||
"RGGB": cv2.COLOR_BayerRG2RGB_EA,
|
||||
"BGGR": cv2.COLOR_BayerBG2RGB_EA,
|
||||
"GRBG": cv2.COLOR_BayerGR2RGB_EA,
|
||||
"GBRG": cv2.COLOR_BayerGB2RGB_EA,
|
||||
}
|
||||
|
||||
if self.bayer_pattern not in mapping:
|
||||
def _validate_contract(self):
|
||||
if self.sensor_width <= 0 or self.sensor_height <= 0:
|
||||
raise ValueError(
|
||||
f"Dimensão de sensor inválida: {self.sensor_width}x{self.sensor_height}"
|
||||
)
|
||||
if self.sensor_width % 4 != 0:
|
||||
raise ValueError(
|
||||
f"RAW10 packed exige largura múltipla de 4: {self.sensor_width}"
|
||||
)
|
||||
if self.bayer_pattern not in self.BAYER_PATTERNS:
|
||||
raise ValueError(f"Padrão Bayer não suportado: {self.bayer_pattern}")
|
||||
|
||||
return mapping[self.bayer_pattern]
|
||||
bit_depth = int(self.config.get("bit_depth", 10))
|
||||
if bit_depth < 8 or bit_depth > 16:
|
||||
raise ValueError(f"bit_depth inválido: {bit_depth}")
|
||||
|
||||
def apply_preview_white_balance(self, bgr: np.ndarray, strength: float = 1.0) -> np.ndarray:
|
||||
levels = self.config["levels"]
|
||||
low = float(levels["black_percentile"])
|
||||
high = float(levels["white_percentile"])
|
||||
if not (0.0 <= low < high <= 100.0):
|
||||
raise ValueError(f"Percentis de níveis inválidos: {low}, {high}")
|
||||
|
||||
gamma = float(self.config["gamma"])
|
||||
if not np.isfinite(gamma) or gamma <= 0.0:
|
||||
raise ValueError(f"Gamma inválido: {gamma}")
|
||||
|
||||
wb = self.config["white_balance"]
|
||||
if float(wb["gain_min"]) <= 0.0 or float(wb["gain_max"]) < float(wb["gain_min"]):
|
||||
raise ValueError("Limites de ganho do white balance inválidos.")
|
||||
|
||||
def get_preview_contract(self) -> dict:
|
||||
"""Contrato serializável para registrar no meta do dataset."""
|
||||
return {
|
||||
"schema": self.CONTRACT_SCHEMA,
|
||||
"version": self.VERSION,
|
||||
"purpose": "human_annotation_only",
|
||||
"geometry": "rgb_sensor_native_no_geometric_transform",
|
||||
"sensor_size": [self.sensor_width, self.sensor_height],
|
||||
"bayer_pattern": self.bayer_pattern,
|
||||
"output": {
|
||||
"layout": "HWC",
|
||||
"color_order": "BGR",
|
||||
"dtype": "uint8",
|
||||
"range": [0, 255],
|
||||
},
|
||||
"calibrations_applied": [],
|
||||
"config": deepcopy(self.config),
|
||||
}
|
||||
|
||||
def get_last_preview_report(self) -> dict:
|
||||
return deepcopy(self.last_preview_report)
|
||||
|
||||
# Nome explícito e amigável para integrações que só precisam registrar
|
||||
# a configuração, sem necessariamente gerar um frame antes.
|
||||
def describe_config(self) -> dict:
|
||||
return self.get_preview_contract()
|
||||
|
||||
@staticmethod
|
||||
def _finite_float(value, name: str) -> float:
|
||||
result = float(value)
|
||||
if not np.isfinite(result):
|
||||
raise ValueError(f"{name} deve ser finito: {value!r}")
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _sample_for_stats(raw: np.ndarray, max_pixels: int) -> np.ndarray:
|
||||
total = int(raw.size)
|
||||
if total <= max_pixels:
|
||||
return raw.reshape(-1)
|
||||
stride = max(1, int(np.ceil(np.sqrt(total / float(max_pixels)))))
|
||||
return raw[::stride, ::stride].reshape(-1)
|
||||
|
||||
def _resolve_levels(
|
||||
self,
|
||||
raw16: np.ndarray,
|
||||
black_level: Optional[float],
|
||||
white_level: Optional[float],
|
||||
bit_depth: int,
|
||||
black_percentile: Optional[float],
|
||||
white_percentile: Optional[float],
|
||||
) -> tuple[float, float, dict]:
|
||||
max_code = float((1 << int(bit_depth)) - 1)
|
||||
levels = self.config["levels"]
|
||||
|
||||
low_pct = float(
|
||||
levels["black_percentile"]
|
||||
if black_percentile is None
|
||||
else black_percentile
|
||||
)
|
||||
high_pct = float(
|
||||
levels["white_percentile"]
|
||||
if white_percentile is None
|
||||
else white_percentile
|
||||
)
|
||||
if not (0.0 <= low_pct < high_pct <= 100.0):
|
||||
raise ValueError(f"Percentis inválidos: {low_pct}, {high_pct}")
|
||||
|
||||
sample = self._sample_for_stats(
|
||||
raw16,
|
||||
max(1, int(levels["sample_max_pixels"])),
|
||||
).astype(np.float32, copy=False)
|
||||
sample = sample[np.isfinite(sample)]
|
||||
if sample.size == 0:
|
||||
raise RuntimeError("RAW não contém pixels finitos para calcular níveis.")
|
||||
|
||||
black_source = "explicit"
|
||||
white_source = "explicit"
|
||||
if black_level is None:
|
||||
black = float(np.percentile(sample, low_pct))
|
||||
black_source = "percentile"
|
||||
else:
|
||||
black = self._finite_float(black_level, "black_level")
|
||||
if white_level is None:
|
||||
white = float(np.percentile(sample, high_pct))
|
||||
white_source = "percentile"
|
||||
else:
|
||||
white = self._finite_float(white_level, "white_level")
|
||||
|
||||
black = float(np.clip(black, 0.0, max_code))
|
||||
white = float(np.clip(white, 0.0, max_code))
|
||||
minimum_span = float(levels["minimum_span_codes"])
|
||||
fallback = False
|
||||
|
||||
if white <= black + minimum_span:
|
||||
black = float(np.clip(np.min(sample), 0.0, max_code))
|
||||
white = float(np.clip(np.max(sample), 0.0, max_code))
|
||||
fallback = True
|
||||
if white <= black:
|
||||
black, white = 0.0, max_code
|
||||
fallback = True
|
||||
|
||||
return black, white, {
|
||||
"black_level": black,
|
||||
"white_level": white,
|
||||
"black_source": black_source,
|
||||
"white_source": white_source,
|
||||
"black_percentile": low_pct,
|
||||
"white_percentile": high_pct,
|
||||
"fallback_to_range": fallback,
|
||||
"sample_pixels": int(sample.size),
|
||||
}
|
||||
|
||||
def raw16_to_vis8(
|
||||
self,
|
||||
raw16: np.ndarray,
|
||||
black_level: Optional[int] = None,
|
||||
white_level: Optional[int] = None,
|
||||
gamma: Optional[float] = None,
|
||||
bit_depth: Optional[int] = None,
|
||||
black_percentile: Optional[float] = None,
|
||||
white_percentile: Optional[float] = None,
|
||||
) -> np.ndarray:
|
||||
"""Converte mosaico RAW16 para mosaico uint8 de visualização."""
|
||||
raw = np.asarray(raw16)
|
||||
if raw.ndim != 2:
|
||||
raise ValueError(f"RAW16 deve ser 2D; shape={raw.shape}")
|
||||
if raw.shape != (self.sensor_height, self.sensor_width):
|
||||
raise ValueError(
|
||||
f"RAW16 shape={raw.shape}; esperado="
|
||||
f"{(self.sensor_height, self.sensor_width)}"
|
||||
)
|
||||
if not np.issubdtype(raw.dtype, np.integer):
|
||||
raise TypeError(f"RAW16 deve possuir dtype inteiro; recebido={raw.dtype}")
|
||||
|
||||
bit_depth = int(
|
||||
self.config["bit_depth"] if bit_depth is None else bit_depth
|
||||
)
|
||||
gamma_value = self._finite_float(
|
||||
self.config["gamma"] if gamma is None else gamma,
|
||||
"gamma",
|
||||
)
|
||||
if gamma_value <= 0.0:
|
||||
raise ValueError("gamma deve ser > 0.")
|
||||
|
||||
black, white, level_report = self._resolve_levels(
|
||||
raw,
|
||||
black_level,
|
||||
white_level,
|
||||
bit_depth,
|
||||
black_percentile,
|
||||
white_percentile,
|
||||
)
|
||||
norm = (raw.astype(np.float32) - black) / max(white - black, 1.0)
|
||||
np.clip(norm, 0.0, 1.0, out=norm)
|
||||
if gamma_value != 1.0:
|
||||
norm = np.power(norm, 1.0 / gamma_value)
|
||||
|
||||
self.last_preview_report = {
|
||||
"stage": "raw16_to_vis8",
|
||||
"bit_depth": bit_depth,
|
||||
"gamma": gamma_value,
|
||||
"levels": level_report,
|
||||
}
|
||||
return np.clip(norm * 255.0 + 0.5, 0, 255).astype(np.uint8)
|
||||
|
||||
def _debayer_code(self):
|
||||
# A API desta classe promete BGR para cv2.imshow/cv2.imwrite.
|
||||
mapping = {
|
||||
"RGGB": ("COLOR_BayerRG2BGR_EA", "COLOR_BayerRG2BGR"),
|
||||
"BGGR": ("COLOR_BayerBG2BGR_EA", "COLOR_BayerBG2BGR"),
|
||||
"GRBG": ("COLOR_BayerGR2BGR_EA", "COLOR_BayerGR2BGR"),
|
||||
"GBRG": ("COLOR_BayerGB2BGR_EA", "COLOR_BayerGB2BGR"),
|
||||
}
|
||||
preferred, fallback = mapping[self.bayer_pattern]
|
||||
return getattr(cv2, preferred, getattr(cv2, fallback))
|
||||
|
||||
def apply_preview_white_balance(
|
||||
self,
|
||||
bgr: np.ndarray,
|
||||
strength: float = 1.0,
|
||||
method: Optional[str] = None,
|
||||
gain_min: Optional[float] = None,
|
||||
gain_max: Optional[float] = None,
|
||||
return_report: bool = False,
|
||||
):
|
||||
"""
|
||||
Gray-world simples para deixar o preview mais agradável.
|
||||
Não usar no raw de treino.
|
||||
WB visual robusto para campo agrícola.
|
||||
|
||||
Primeiro procura pixels claros e de baixo croma, reduzindo o risco de
|
||||
o gray-world neutralizar toda a vegetação verde. Se não houver amostra
|
||||
neutra suficiente, usa gray-world com ganhos limitados.
|
||||
"""
|
||||
img = bgr.astype(np.float32)
|
||||
image = np.asarray(bgr)
|
||||
if image.ndim != 3 or image.shape[2] != 3:
|
||||
raise ValueError(f"WB requer BGR HWC; shape={image.shape}")
|
||||
|
||||
mean_b = float(img[:, :, 0].mean())
|
||||
mean_g = float(img[:, :, 1].mean())
|
||||
mean_r = float(img[:, :, 2].mean())
|
||||
cfg = self.config["white_balance"]
|
||||
method = str(method or cfg["method"]).lower()
|
||||
strength = float(np.clip(self._finite_float(strength, "wb_strength"), 0.0, 1.0))
|
||||
gain_min = float(cfg["gain_min"] if gain_min is None else gain_min)
|
||||
gain_max = float(cfg["gain_max"] if gain_max is None else gain_max)
|
||||
if gain_min <= 0.0 or gain_max < gain_min:
|
||||
raise ValueError("Limites de ganho WB inválidos.")
|
||||
|
||||
mean_gray = (mean_b + mean_g + mean_r) / 3.0
|
||||
sample = image[::4, ::4].astype(np.float32)
|
||||
flat = sample.reshape(-1, 3)
|
||||
intensity = flat.mean(axis=1)
|
||||
chroma = (flat.max(axis=1) - flat.min(axis=1)) / np.maximum(intensity, 1.0)
|
||||
|
||||
eps = 1e-6
|
||||
gain_b = mean_gray / max(mean_b, eps)
|
||||
gain_g = mean_gray / max(mean_g, eps)
|
||||
gain_r = mean_gray / max(mean_r, eps)
|
||||
selected = flat
|
||||
source = "gray_world"
|
||||
if method.startswith("neutral_bright") and flat.shape[0] > 0:
|
||||
bright_limit = float(np.percentile(intensity, float(cfg["bright_percentile"])))
|
||||
bright_mask = intensity >= bright_limit
|
||||
bright_chroma = chroma[bright_mask]
|
||||
if bright_chroma.size:
|
||||
neutral_limit = float(
|
||||
np.percentile(
|
||||
bright_chroma,
|
||||
float(cfg["neutral_chroma_percentile"]),
|
||||
)
|
||||
)
|
||||
neutral_mask = bright_mask & (chroma <= neutral_limit)
|
||||
neutral = flat[neutral_mask]
|
||||
if neutral.shape[0] >= int(cfg["min_neutral_pixels"]):
|
||||
selected = neutral
|
||||
source = "neutral_bright"
|
||||
|
||||
# strength=1 aplica total, strength=0 não aplica
|
||||
gain_b = 1.0 + (gain_b - 1.0) * strength
|
||||
gain_g = 1.0 + (gain_g - 1.0) * strength
|
||||
gain_r = 1.0 + (gain_r - 1.0) * strength
|
||||
means = selected.mean(axis=0)
|
||||
target = float(means.mean())
|
||||
gains = target / np.maximum(means, 1e-6)
|
||||
gains = np.clip(gains, gain_min, gain_max)
|
||||
gains = 1.0 + (gains - 1.0) * strength
|
||||
|
||||
img[:, :, 0] *= gain_b
|
||||
img[:, :, 1] *= gain_g
|
||||
img[:, :, 2] *= gain_r
|
||||
out = np.clip(
|
||||
image.astype(np.float32) * gains[None, None, :],
|
||||
0,
|
||||
255,
|
||||
).astype(np.uint8)
|
||||
report = {
|
||||
"method_requested": method,
|
||||
"source_used": source,
|
||||
"strength": strength,
|
||||
"means_bgr": [float(x) for x in means],
|
||||
"gains_bgr": [float(x) for x in gains],
|
||||
"selected_pixels": int(selected.shape[0]),
|
||||
}
|
||||
return (out, report) if return_report else out
|
||||
|
||||
return np.clip(img, 0, 255).astype(np.uint8)
|
||||
def apply_preview_contrast(
|
||||
self,
|
||||
bgr: np.ndarray,
|
||||
alpha: float = 1.08,
|
||||
beta: float = 0.0,
|
||||
) -> np.ndarray:
|
||||
"""Compatibilidade: contraste/brilho visual sem alterar geometria."""
|
||||
alpha = self._finite_float(alpha, "contrast_alpha")
|
||||
beta = self._finite_float(beta, "contrast_beta")
|
||||
return cv2.convertScaleAbs(bgr, alpha=alpha, beta=beta)
|
||||
|
||||
def apply_preview_contrast(self, bgr: np.ndarray, alpha: float = 1.08, beta: float = 0.0) -> np.ndarray:
|
||||
"""
|
||||
Ajuste leve de contraste/brilho para preview.
|
||||
"""
|
||||
out = cv2.convertScaleAbs(bgr, alpha=alpha, beta=beta)
|
||||
return out
|
||||
@staticmethod
|
||||
def _apply_clahe_luminance(
|
||||
bgr: np.ndarray,
|
||||
clip_limit: float,
|
||||
tile_grid_size: int,
|
||||
) -> np.ndarray:
|
||||
lab = cv2.cvtColor(bgr, cv2.COLOR_BGR2LAB)
|
||||
luma, a_ch, b_ch = cv2.split(lab)
|
||||
clahe = cv2.createCLAHE(
|
||||
clipLimit=float(clip_limit),
|
||||
tileGridSize=(int(tile_grid_size), int(tile_grid_size)),
|
||||
)
|
||||
return cv2.cvtColor(
|
||||
cv2.merge([clahe.apply(luma), a_ch, b_ch]),
|
||||
cv2.COLOR_LAB2BGR,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _apply_saturation(bgr: np.ndarray, factor: float) -> np.ndarray:
|
||||
hsv = cv2.cvtColor(bgr, cv2.COLOR_BGR2HSV)
|
||||
sat = hsv[:, :, 1].astype(np.float32) * float(factor)
|
||||
hsv[:, :, 1] = np.clip(sat, 0, 255).astype(np.uint8)
|
||||
return cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
|
||||
|
||||
@staticmethod
|
||||
def _apply_unsharp(
|
||||
bgr: np.ndarray,
|
||||
sigma: float,
|
||||
amount: float,
|
||||
threshold: float,
|
||||
) -> np.ndarray:
|
||||
image = bgr.astype(np.float32)
|
||||
blurred = cv2.GaussianBlur(
|
||||
image,
|
||||
(0, 0),
|
||||
sigmaX=float(sigma),
|
||||
sigmaY=float(sigma),
|
||||
)
|
||||
sharpened = image + float(amount) * (image - blurred)
|
||||
if threshold > 0.0:
|
||||
mask = np.max(np.abs(image - blurred), axis=2) >= float(threshold)
|
||||
result = image.copy()
|
||||
result[mask] = sharpened[mask]
|
||||
else:
|
||||
result = sharpened
|
||||
return np.clip(result, 0, 255).astype(np.uint8)
|
||||
|
||||
def raw16_to_preview_bgr(
|
||||
self,
|
||||
raw16: np.ndarray,
|
||||
gamma: float = 2.2,
|
||||
wb_strength: float = 0.8,
|
||||
gamma: Optional[float] = None,
|
||||
wb_strength: Optional[float] = None,
|
||||
apply_wb: bool = True,
|
||||
apply_contrast: bool = True,
|
||||
bit_depth: int = 10,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Pipeline de preview bonito:
|
||||
1. auto-level + gamma no mosaico
|
||||
2. demosaic
|
||||
3. white balance simples
|
||||
4. leve contraste final
|
||||
"""
|
||||
vis8 = self.raw16_to_vis8(raw16, gamma=gamma, bit_depth=bit_depth)
|
||||
bit_depth: Optional[int] = None,
|
||||
*,
|
||||
black_level: Optional[float] = None,
|
||||
white_level: Optional[float] = None,
|
||||
black_percentile: Optional[float] = None,
|
||||
white_percentile: Optional[float] = None,
|
||||
apply_clahe: Optional[bool] = None,
|
||||
apply_saturation: Optional[bool] = None,
|
||||
apply_sharpen: Optional[bool] = None,
|
||||
return_report: bool = False,
|
||||
):
|
||||
"""Gera preview BGR bonito preservando exatamente o raster de entrada."""
|
||||
source = np.asarray(raw16)
|
||||
expected_shape = (self.sensor_height, self.sensor_width)
|
||||
if source.shape != expected_shape:
|
||||
raise ValueError(f"RAW16 shape={source.shape}; esperado={expected_shape}")
|
||||
|
||||
effective_gamma = float(
|
||||
self.config["gamma"] if gamma is None else gamma
|
||||
)
|
||||
effective_bit_depth = int(
|
||||
self.config["bit_depth"] if bit_depth is None else bit_depth
|
||||
)
|
||||
effective_wb_strength = float(
|
||||
self.config["white_balance"]["strength"]
|
||||
if wb_strength is None
|
||||
else wb_strength
|
||||
)
|
||||
|
||||
vis8 = self.raw16_to_vis8(
|
||||
source,
|
||||
black_level=black_level,
|
||||
white_level=white_level,
|
||||
gamma=effective_gamma,
|
||||
bit_depth=effective_bit_depth,
|
||||
black_percentile=black_percentile,
|
||||
white_percentile=white_percentile,
|
||||
)
|
||||
level_report = deepcopy(self.last_preview_report.get("levels", {}))
|
||||
bgr = cv2.cvtColor(vis8, self._debayer_code())
|
||||
|
||||
wb_report = {"enabled": False}
|
||||
if apply_wb:
|
||||
bgr = self.apply_preview_white_balance(bgr, strength=wb_strength)
|
||||
bgr, wb_report = self.apply_preview_white_balance(
|
||||
bgr,
|
||||
strength=effective_wb_strength,
|
||||
return_report=True,
|
||||
)
|
||||
wb_report["enabled"] = True
|
||||
|
||||
clahe_cfg = self.config["clahe"]
|
||||
clahe_enabled = bool(clahe_cfg["enabled"] if apply_clahe is None else apply_clahe)
|
||||
if clahe_enabled:
|
||||
bgr = self._apply_clahe_luminance(
|
||||
bgr,
|
||||
float(clahe_cfg["clip_limit"]),
|
||||
int(clahe_cfg["tile_grid_size"]),
|
||||
)
|
||||
|
||||
saturation_cfg = self.config["saturation"]
|
||||
saturation_enabled = bool(
|
||||
saturation_cfg["enabled"]
|
||||
if apply_saturation is None
|
||||
else apply_saturation
|
||||
)
|
||||
if saturation_enabled:
|
||||
bgr = self._apply_saturation(bgr, float(saturation_cfg["factor"]))
|
||||
|
||||
contrast_cfg = self.config["contrast"]
|
||||
if apply_contrast:
|
||||
bgr = self.apply_preview_contrast(bgr, alpha=1.08, beta=0.0)
|
||||
bgr = self.apply_preview_contrast(
|
||||
bgr,
|
||||
alpha=float(contrast_cfg["alpha"]),
|
||||
beta=float(contrast_cfg["beta"]),
|
||||
)
|
||||
|
||||
return bgr
|
||||
sharpen_cfg = self.config["sharpen"]
|
||||
sharpen_enabled = bool(
|
||||
sharpen_cfg["enabled"] if apply_sharpen is None else apply_sharpen
|
||||
)
|
||||
if sharpen_enabled:
|
||||
bgr = self._apply_unsharp(
|
||||
bgr,
|
||||
sigma=float(sharpen_cfg["sigma"]),
|
||||
amount=float(sharpen_cfg["amount"]),
|
||||
threshold=float(sharpen_cfg["threshold"]),
|
||||
)
|
||||
|
||||
def raw16_to_preview_jpg_bytes(self, raw16: np.ndarray, jpeg_quality: int = 95) -> bytes:
|
||||
if bgr.shape != (self.sensor_height, self.sensor_width, 3):
|
||||
raise RuntimeError(
|
||||
f"Preview alterou geometria: {bgr.shape}; esperado="
|
||||
f"{(self.sensor_height, self.sensor_width, 3)}"
|
||||
)
|
||||
if bgr.dtype != np.uint8:
|
||||
raise RuntimeError(f"Preview dtype={bgr.dtype}; esperado=uint8")
|
||||
|
||||
self.last_preview_report = {
|
||||
**self.get_preview_contract(),
|
||||
"effective": {
|
||||
"bit_depth": effective_bit_depth,
|
||||
"gamma": effective_gamma,
|
||||
"levels": level_report,
|
||||
"white_balance": wb_report,
|
||||
"clahe_enabled": clahe_enabled,
|
||||
"saturation_enabled": saturation_enabled,
|
||||
"contrast_enabled": bool(apply_contrast),
|
||||
"sharpen_enabled": sharpen_enabled,
|
||||
},
|
||||
}
|
||||
result = np.ascontiguousarray(bgr)
|
||||
return (result, self.get_last_preview_report()) if return_report else result
|
||||
|
||||
def unpack_raw10_packed(
|
||||
self,
|
||||
packed_frame,
|
||||
sensor_width: Optional[int] = None,
|
||||
sensor_height: Optional[int] = None,
|
||||
stride: Optional[int] = None,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Desempacota RAW10 MIPI (4 pixels/5 bytes), aceitando ndarray 1D/2D,
|
||||
bytes e stride/padding por linha.
|
||||
"""
|
||||
width = self.sensor_width if sensor_width is None else int(sensor_width)
|
||||
height = self.sensor_height if sensor_height is None else int(sensor_height)
|
||||
if width != self.sensor_width or height != self.sensor_height:
|
||||
raise ValueError(
|
||||
"RawProcessorPreview preserva o raster configurado; dimensão "
|
||||
f"solicitada={width}x{height}, configurada="
|
||||
f"{self.sensor_width}x{self.sensor_height}."
|
||||
)
|
||||
if width % 4 != 0:
|
||||
raise ValueError(f"RAW10 exige largura múltipla de 4: {width}")
|
||||
|
||||
if isinstance(packed_frame, (bytes, bytearray, memoryview)):
|
||||
packed = np.frombuffer(packed_frame, dtype=np.uint8)
|
||||
else:
|
||||
packed = np.asarray(packed_frame)
|
||||
if packed.ndim == 3 and packed.shape[2] == 1:
|
||||
packed = packed[:, :, 0]
|
||||
|
||||
useful_width = (width // 4) * 5
|
||||
|
||||
if packed.dtype != np.uint8:
|
||||
raise TypeError(
|
||||
f"RAW10 packed deve ser uint8; shape={packed.shape}, dtype={packed.dtype}"
|
||||
)
|
||||
|
||||
if packed.ndim == 1:
|
||||
if stride is None:
|
||||
if packed.size % height != 0:
|
||||
raise ValueError(
|
||||
f"RAW10 1D possui {packed.size} bytes, não divisível por "
|
||||
f"height={height}; informe stride."
|
||||
)
|
||||
stride = packed.size // height
|
||||
stride = int(stride)
|
||||
expected_bytes = stride * height
|
||||
if stride < useful_width or packed.size < expected_bytes:
|
||||
raise ValueError(
|
||||
f"RAW10 1D curto/inválido: bytes={packed.size}, stride={stride}, "
|
||||
f"esperado>={expected_bytes}, payload/linha={useful_width}."
|
||||
)
|
||||
packed = packed[:expected_bytes].reshape(height, stride)
|
||||
elif packed.ndim == 2:
|
||||
if stride is not None and int(stride) != packed.shape[1]:
|
||||
raise ValueError(
|
||||
f"stride={stride} diverge da largura do array={packed.shape[1]}."
|
||||
)
|
||||
else:
|
||||
raise TypeError(
|
||||
f"RAW10 packed deve ser 1D ou 2D; shape={packed.shape}."
|
||||
)
|
||||
|
||||
if packed.shape[0] != height or packed.shape[1] < useful_width:
|
||||
raise ValueError(
|
||||
f"RAW10 packed shape={packed.shape}; esperado >=({height}, {useful_width})"
|
||||
)
|
||||
padding = int(packed.shape[1] - useful_width)
|
||||
if padding > max(4096, useful_width):
|
||||
raise ValueError(
|
||||
f"Padding RAW10 implausível: {padding} bytes/linha; "
|
||||
"verifique width/height/stride."
|
||||
)
|
||||
|
||||
payload = np.ascontiguousarray(packed[:, :useful_width])
|
||||
groups = payload.reshape(height, width // 4, 5)
|
||||
high = groups[:, :, :4].astype(np.uint16)
|
||||
low = groups[:, :, 4].astype(np.uint16)
|
||||
|
||||
out = np.empty((height, width // 4, 4), dtype=np.uint16)
|
||||
out[:, :, 0] = (high[:, :, 0] << 2) | (low & 0x03)
|
||||
out[:, :, 1] = (high[:, :, 1] << 2) | ((low >> 2) & 0x03)
|
||||
out[:, :, 2] = (high[:, :, 2] << 2) | ((low >> 4) & 0x03)
|
||||
out[:, :, 3] = (high[:, :, 3] << 2) | ((low >> 6) & 0x03)
|
||||
return np.ascontiguousarray(out.reshape(height, width))
|
||||
|
||||
def packed_raw10_to_preview_bgr(self, packed_frame: np.ndarray, **kwargs):
|
||||
"""Atalho oficial RAW10 packed -> preview BGR."""
|
||||
raw16 = self.unpack_raw10_packed(packed_frame)
|
||||
return self.raw16_to_preview_bgr(raw16, **kwargs)
|
||||
|
||||
def raw16_to_preview_jpg_bytes(
|
||||
self,
|
||||
raw16: np.ndarray,
|
||||
jpeg_quality: int = 95,
|
||||
) -> bytes:
|
||||
quality = int(np.clip(int(jpeg_quality), 1, 100))
|
||||
bgr = self.raw16_to_preview_bgr(raw16)
|
||||
ok, enc = cv2.imencode(".jpg", bgr, [int(cv2.IMWRITE_JPEG_QUALITY), int(jpeg_quality)])
|
||||
ok, encoded = cv2.imencode(
|
||||
".jpg",
|
||||
bgr,
|
||||
[int(cv2.IMWRITE_JPEG_QUALITY), quality],
|
||||
)
|
||||
if not ok:
|
||||
raise RuntimeError("Falha ao codificar preview JPG")
|
||||
return enc.tobytes()
|
||||
return encoded.tobytes()
|
||||
|
||||
def raw16_to_preview_png_bytes(
|
||||
self,
|
||||
raw16: np.ndarray,
|
||||
compression: int = 3,
|
||||
) -> bytes:
|
||||
level = int(np.clip(int(compression), 0, 9))
|
||||
bgr = self.raw16_to_preview_bgr(raw16)
|
||||
ok, encoded = cv2.imencode(
|
||||
".png",
|
||||
bgr,
|
||||
[int(cv2.IMWRITE_PNG_COMPRESSION), level],
|
||||
)
|
||||
if not ok:
|
||||
raise RuntimeError("Falha ao codificar preview PNG")
|
||||
return encoded.tobytes()
|
||||
|
|
|
|||
Loading…
Reference in New Issue