def groupTrackersToTransformers(transformer_power, max_distance):
    import numpy as np
    from scipy.spatial import KDTree
    import FreeCAD
    from collections import deque

    # 1. Obtener todos los trackers válidos
    valid_trackers = []
    valid_points = []
    valid_power = []

    for tracker in FreeCAD.ActiveDocument.Objects:
        if hasattr(tracker, 'Proxy') and (tracker.Proxy.Type == "Tracker"):
            base = tracker.Placement.Base
            if all(np.isfinite([base.x, base.y, base.z])):
                valid_trackers.append(tracker)
                valid_points.append([base.x, base.y])
                valid_power.append(tracker.Setup.TotalPower)

    if not valid_trackers:
        FreeCAD.Console.PrintWarning("No se encontraron trackers válidos para agrupar\n")
        return

    # 2. Obtener parámetros de los CTs
    target_power = transformer_power * 1.2
    points = np.array(valid_points)
    power_values = np.array(valid_power)

    # 3. Determinar dirección de barrido (oeste a este por defecto)
    min_x, min_y = np.min(points, axis=0)
    max_x, max_y = np.max(points, axis=0)

    # 4. Ordenar trackers de oeste a este (menor X a mayor X)
    '''
    Norte a Sur: sorted_indices = np.argsort(-points[:, 1]) (Y descendente)
    Sur a Norte: sorted_indices = np.argsort(points[:, 1]) (Y ascendente)
    Este a Oeste: sorted_indices = np.argsort(-points[:, 0]) (X descendente)
    '''
    sorted_indices = np.argsort(points[:, 0])
    sorted_points = points[sorted_indices]
    sorted_power = power_values[sorted_indices]
    sorted_trackers = [valid_trackers[i] for i in sorted_indices]

    # 5. Crear KDTree para búsquedas rápidas
    kdtree = KDTree(sorted_points)

    # 6. Algoritmo de barrido espacial
    transformer_groups = []
    used_indices = set()

    # Función para expandir un grupo desde un punto inicial
    def expand_group(start_idx):
        group = []
        total_power = 0
        queue = deque([start_idx])

        while queue and total_power < target_power:
            idx = queue.popleft()
            if idx in used_indices:
                continue

            # Añadir tracker al grupo si no excede la potencia
            tracker_power = sorted_power[idx]
            if total_power + tracker_power > target_power * 1.05:
                continue

            group.append(sorted_trackers[idx])
            total_power += tracker_power
            used_indices.add(idx)

            # Buscar vecinos cercanos
            neighbors = kdtree.query_ball_point(
                sorted_points[idx],
                max_distance
            )

            # Filtrar vecinos no usados y ordenar por proximidad al punto inicial
            neighbors = [n for n in neighbors if n not in used_indices]
            neighbors.sort(key=lambda n: abs(n - start_idx))
            queue.extend(neighbors)

        return group, total_power

    # 7. Barrido principal de oeste a este
    for i in range(len(sorted_points)):
        if i in used_indices:
            continue

        group, total_power = expand_group(i)

        if group:
            # Calcular centro del grupo
            group_points = np.array([t.Placement.Base[:2] for t in group])
            center = np.mean(group_points, axis=0)

            transformer_groups.append({
                'trackers': group,
                'total_power': total_power,
                'center': center
            })

    # 8. Manejar grupos residuales (si los hay)
    unused_indices = set(range(len(sorted_points))) - used_indices
    if unused_indices:
        # Intentar añadir trackers residuales a grupos existentes
        for idx in unused_indices:
            point = sorted_points[idx]
            tracker_power = sorted_power[idx]

            # Buscar el grupo más cercano que pueda aceptar este tracker
            best_group = None
            min_distance = float('inf')

            for group in transformer_groups:
                if group['total_power'] + tracker_power <= target_power * 1.05:
                    dist = np.linalg.norm(point - group['center'])
                    if dist < min_distance and dist < max_distance * 1.5:
                        min_distance = dist
                        best_group = group

            # Añadir al grupo si se encontró uno adecuado
            if best_group:
                best_group['trackers'].append(sorted_trackers[idx])
                best_group['total_power'] += tracker_power
                # Actualizar centro del grupo
                group_points = np.array([t.Placement.Base[:2] for t in best_group['trackers']])
                best_group['center'] = np.mean(group_points, axis=0)
            else:
                # Crear un nuevo grupo con este tracker residual
                group = [sorted_trackers[idx]]
                center = point
                transformer_groups.append({
                    'trackers': group,
                    'total_power': tracker_power,
                    'center': center
                })

    # 9. Crear los grupos en FreeCAD
    transformer_group = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", "Transformers")
    transformer_group.Label = "Centros de Transformación"

    for i, group in enumerate(transformer_groups):
        # Crear la esfera que representará el CT
        ct_shape = FreeCAD.ActiveDocument.addObject("Part::Box", f"CT_{i + 1}")
        ct_shape.Length = 6058
        ct_shape.Width = 2438
        ct_shape.Height = 2591
        ct_shape.Placement.Base = FreeCAD.Vector(group['center'][0], group['center'][1], 0)

        # Añadir propiedades personalizadas
        ct_shape.addProperty("App::PropertyLinkList", "Trackers", "CT",
                              "Lista de trackers asociados a este CT")
        ct_shape.addProperty("App::PropertyFloat", "TotalPower", "CT",
                              "Potencia total del grupo (W)")
        ct_shape.addProperty("App::PropertyFloat", "NominalPower", "CT",
                              "Potencia nominal del transformador (W)")
        ct_shape.addProperty("App::PropertyFloat", "Utilization", "CT",
                              "Porcentaje de utilización (Total/Nominal)")

        # Establecer valores de las propiedades
        ct_shape.Trackers = group['trackers']
        ct_shape.TotalPower = group['total_power'].Value
        ct_shape.NominalPower = transformer_power
        ct_shape.Utilization = (group['total_power'].Value / transformer_power) * 100

        # Configurar visualización
        # Calcular color basado en utilización (verde < 100%, amarillo < 110%, rojo > 110%)
        utilization = ct_shape.Utilization
        if utilization <= 100:
            color = (0.0, 1.0, 0.0)  # Verde
        elif utilization <= 110:
            color = (1.0, 1.0, 0.0)  # Amarillo
        else:
            color = (1.0, 0.0, 0.0)  # Rojo

        ct_shape.ViewObject.ShapeColor = color
        ct_shape.ViewObject.Transparency = 40  # 40% de transparencia

        # Añadir etiqueta con información
        ct_shape.ViewObject.DisplayMode = "Shaded"
        ct_shape.Label = f"CT {i + 1} ({ct_shape.TotalPower / 1000:.1f}kW/{ct_shape.NominalPower / 1000:.1f}kW)"

        # Añadir al grupo principal
        transformer_group.addObject(ct_shape)

    FreeCAD.Console.PrintMessage(f"Se crearon {len(transformer_groups)} centros de transformación\n")

    #onSelectGatePoint()


import FreeCAD, FreeCADGui, Part
import numpy as np
from scipy.stats import linregress
from PySide import QtGui

class InternalPathCreator:
    def __init__(self, gate_point, strategy=1, path_width=4000):
        self.gate_point = gate_point
        self.strategy = strategy
        self.path_width = path_width
        self.ct_shapes = []
        self.ct_positions = []

    def get_transformers(self):
        transformers_group = FreeCAD.ActiveDocument.getObject("Transformers")
        if not transformers_group:
            FreeCAD.Console.PrintError("No se encontró el grupo 'Transformers'\n")
            return False

        self.ct_shapes = transformers_group.Group
        if not self.ct_shapes:
            FreeCAD.Console.PrintWarning("No hay Centros de Transformación en el grupo\n")
            return False

        # Obtener las posiciones de los CTs
        for sphere in self.ct_shapes:
            base = sphere.Placement.Base
            self.ct_positions.append(FreeCAD.Vector(base.x, base.y, 0))
        return True

    def create_paths(self):
        if not self.get_transformers():
            return []

        if self.strategy == 1:
            return self.create_direct_paths()
        elif self.strategy == 2:
            return self.create_unified_path()
        else:
            FreeCAD.Console.PrintError("Estrategia no válida. Use 1 o 2.\n")
            return []

    def create_direct_paths(self):
        """Estrategia 1: Caminos independientes desde cada CT hasta la puerta"""
        paths = []
        for ct in self.ct_positions:
            paths.append([ct, self.gate_point])
        return paths

    def create_unified_path(self):
        """Estrategia 2: Único camino que une todos los CTs y la puerta usando regresión lineal"""
        if not self.ct_positions:
            return []

        all_points = self.ct_positions + [self.gate_point]
        x = [p.x for p in all_points]
        y = [p.y for p in all_points]

        # Manejar caso de puntos alineados verticalmente
        if np.std(x) < 1e-6:
            sorted_points = sorted(all_points, key=lambda p: p.y)
            paths = []
            for i in range(len(sorted_points) - 1):
                paths.append([sorted_points[i], sorted_points[i + 1]])
            return paths

        # Calcular regresión lineal
        slope, intercept, _, _, _ = linregress(x, y)

        # Función para proyectar puntos
        def project_point(point):
            x0, y0 = point.x, point.y
            if abs(slope) > 1e6:
                return FreeCAD.Vector(x0, intercept, 0)
            x_proj = (x0 + slope * (y0 - intercept)) / (1 + slope ** 2)
            y_proj = slope * x_proj + intercept
            return FreeCAD.Vector(x_proj, y_proj, 0)

            # return slope * x + intercept --> desde placement

        projected_points = [project_point(p) for p in all_points]

        # Calcular distancias a lo largo de la línea
        ref_point = projected_points[0]
        direction_vector = FreeCAD.Vector(1, slope).normalize()
        distances = []
        for p in projected_points:
            vec_to_point = p - ref_point
            distance = vec_to_point.dot(direction_vector)
            distances.append(distance)

        # Ordenar por distancia
        sorted_indices = np.argsort(distances)
        sorted_points = [all_points[i] for i in sorted_indices]

        # Crear caminos
        paths = []
        for i in range(len(sorted_points) - 1):
            paths.append([sorted_points[i], sorted_points[i + 1]])
        return paths

    def create_3d_path(self, path_poly):
        """Crea geometría 3D para el camino adaptada a orientación norte-sur"""
        segments = []
        for i in range(len(path_poly.Vertexes) - 1):
            start = path_poly.Vertexes[i].Point
            end = path_poly.Vertexes[i + 1].Point
            direction = end - start

            # Determinar orientación predominante
            if abs(direction.x) > abs(direction.y):
                normal = FreeCAD.Vector(0, 1, 0)  # Norte-sur
            else:
                normal = FreeCAD.Vector(1, 0, 0)  # Este-oeste

            offset = normal * self.path_width / 2

            # Crear puntos para la sección transversal
            p1 = start + offset
            p2 = start - offset
            p3 = end - offset
            p4 = end + offset

            # Crear cara
            wire = Part.makePolygon([p1, p2, p3, p4, p1])
            face = Part.Face(wire)
            segments.append(face)

        if segments:
            '''road_shape = segments[0].fuse(segments[1:])
            return road_shape.removeSplitter()'''
            return Part.makeCompound(segments)
        return Part.Shape()

    def build(self):
        paths = self.create_paths()
        if not paths:
            return

        path_group = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", "InternalPaths")
        path_group.Label = f"Caminos Internos (Estrategia {self.strategy})"

        for i, path in enumerate(paths):
            poly = Part.makePolygon(path)

            # Objeto para la línea central
            path_obj = FreeCAD.ActiveDocument.addObject("Part::Feature", f"Path_{i + 1}")
            path_obj.Shape = poly
            path_obj.ViewObject.LineWidth = 3.0
            path_obj.ViewObject.LineColor = (0.0, 0.0, 1.0)

            # Objeto para la superficie 3D
            road_shape = self.create_3d_path(poly)
            road_obj = FreeCAD.ActiveDocument.addObject("Part::Feature", f"Road_{i + 1}")
            road_obj.Shape = road_shape
            road_obj.ViewObject.ShapeColor = (0.7, 0.7, 0.7)

            path_group.addObject(path_obj)
            path_group.addObject(road_obj)

        FreeCAD.Console.PrintMessage(f"Se crearon {len(paths)} segmentos de caminos internos\n")


# Función para mostrar el diálogo de estrategia
def show_path_strategy_dialog(gate_point):
    dialog = QtGui.QDialog()
    dialog.setWindowTitle("Seleccionar Estrategia de Caminos")
    layout = QtGui.QVBoxLayout(dialog)

    label = QtGui.QLabel("Seleccione la estrategia para crear los caminos internos:")
    layout.addWidget(label)

    rb1 = QtGui.QRadioButton("Estrategia 1: Caminos independientes desde cada CT hasta la puerta")
    rb1.setChecked(True)
    layout.addWidget(rb1)

    rb2 = QtGui.QRadioButton("Estrategia 2: Único camino que une todos los CTs y la puerta")
    layout.addWidget(rb2)

    btn_box = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel)
    layout.addWidget(btn_box)

    def on_accept():
        strategy = 1 if rb1.isChecked() else 2
        dialog.accept()
        creator = InternalPathCreator(gate_point, strategy)
        creator.build()

    btn_box.accepted.connect(on_accept)
    btn_box.rejected.connect(dialog.reject)

    dialog.exec_()


# Uso: seleccionar un punto para la puerta de entrada
def onSelectGatePoint():
    '''gate = FreeCAD.ActiveDocument.findObjects(Name="FenceGate")[0]
    gate_point = gate.Placement.Base
    show_path_strategy_dialog(gate_point)'''

    sel = FreeCADGui.Selection.getSelectionEx()[0]
    show_path_strategy_dialog(sel.SubObjects[0].CenterOfMass)