import { Stack } from 'stack-typescript';
import { Point } from '../../..';
import { Polyline, LineSegment, Rectangle } from '../../../math/geometry';
import { distPP } from '../../../math/geometry/point';
import { PointPair } from '../../../math/geometry/pointPair';
import { PolylinePoint } from '../../../math/geometry/polylinePoint';
import { CreateRectangleNodeOnEnumeration, mkRectangleNode } from '../../../math/geometry/RTree/RectangleNode';
import { CrossRectangleNodesSameType } from '../../../math/geometry/RTree/RectangleNodeUtils';
import { Assert } from '../../../utils/assert';
import { PointPairMap } from '../../../utils/pointPairMap';
import { PointSet } from '../../../utils/PointSet';
import { addToPointPairMap, addToPointMapTuple, removeFromPointPairMapTuple, setIntersection, setIntersectionOfArray, substractSets, uniteSets, } from '../../../utils/setOperations';
import { InteractiveEdgeRouter } from '../../InteractiveEdgeRouter';
import { CostCalculator } from './CostCalculator';
import { HubRadiiCalculator } from './HubRadiiCalculator';
import { PathFixer } from './PathFixer';
import { SimulatedAnnealing } from './SimulatedAnnealing';
import { TupleMap } from './tupleMap';
// Adjust current bundle-routing with a number of heuristic
export class NodePositionsAdjuster {
    constructor(metroGraphData, bundlingSettings) {
        this.metroGraphData = metroGraphData;
        this.bundlingSettings = bundlingSettings;
    }
    // apply a number of heuristics to improve current routing
    static FixRouting(metroGraphData, bundlingSettings) {
        // TimeMeasurer.DebugOutput("Initial cost = " + CostCalculator.Cost(metroGraphData, bundlingSettings));
        // TimeMeasurer.DebugOutput("Initial cost of forces: " + CostCalculator.CostOfForces(metroGraphData, bundlingSettings));
        const adjuster = new NodePositionsAdjuster(metroGraphData, bundlingSettings);
        adjuster.GlueConflictingNodes();
        adjuster.UnglueEdgesFromBundleToSaveInk(true);
        let step = 0;
        const MaxSteps = 10;
        while (++step < MaxSteps) {
            // heuristics to improve routing
            let progress = adjuster.GlueConflictingNodes();
            progress || (progress = adjuster.RelaxConstrainedEdges());
            progress || (progress = step <= 3 && adjuster.UnglueEdgesFromBundleToSaveInk(false));
            progress || (progress = adjuster.GlueCollinearNeighbors(step));
            progress || (progress = step == 3 && adjuster.RemoveDoublePathCrossings());
            if (!progress) {
                break;
            }
        }
        //one SA has to be executed with bundle forces
        metroGraphData.cdtIntersections.ComputeForcesForBundles = true;
        adjuster.RemoveDoublePathCrossings();
        adjuster.UnglueEdgesFromBundleToSaveInk(true);
        while (adjuster.GlueConflictingNodes()) { }
        metroGraphData.Initialize(true);
        //this time initialize the tight enterables also
        // this time initialize the tight enterables also
        //             HubDebugger.ShowHubs(metroGraphData, bundlingSettings);
        // TimeMeasurer.DebugOutput("NodePositionsAdjuster stopped after " + step + " steps");
        // HubDebugger.ShowHubs(metroGraphData, bundlingSettings, true);
        // TimeMeasurer.DebugOutput("Final cost: " + CostCalculator.Cost(metroGraphData, bundlingSettings));
        // TimeMeasurer.DebugOutput("Final cost of forces: " + CostCalculator.CostOfForces(metroGraphData, bundlingSettings));
    }
    // unite the nodes that are close to each other
    GlueConflictingNodes() {
        const circlesHierarchy = this.GetCirclesHierarchy();
        if (circlesHierarchy == null) {
            return false;
        }
        const gluingMap = new Map();
        const gluedDomain = new Set();
        CrossRectangleNodesSameType(circlesHierarchy, circlesHierarchy, (i, j) => this.TryToGlueNodes(i, j, gluingMap, gluedDomain));
        if (gluingMap.size == 0) {
            return false;
        }
        for (let i = 0; i < this.metroGraphData.Edges.length; i++) {
            this.RegenerateEdge(gluingMap, i);
        }
        // can it be more efficient?
        //can it be more efficient?
        const affectedPoints = new PointSet();
        for (const s of gluedDomain) {
            affectedPoints.add(s.Position);
            for (const neig of s.Neighbors)
                if (!neig.IsRealNode)
                    affectedPoints.add(neig.Position);
        }
        // TimeMeasurer.DebugOutput("gluing nodes");
        this.metroGraphData.Initialize(false);
        SimulatedAnnealing.FixRoutingMBP(this.metroGraphData, this.bundlingSettings, affectedPoints);
        return true;
    }
    GetCirclesHierarchy() {
        for (const v of this.metroGraphData.VirtualNodes())
            v.Radius = this.GetCurrentHubRadius(v);
        const t = this.metroGraphData.VirtualNodes().map(rectNodeOfStation);
        return CreateRectangleNodeOnEnumeration(t);
        function rectNodeOfStation(i) {
            const p = i.Position;
            const r = Math.max(i.Radius, 5);
            const del = new Point(r, r);
            const b = Rectangle.mkPP(p.add(del), p.sub(del));
            return mkRectangleNode(i, b);
        }
    }
    GetCurrentHubRadius(node) {
        if (node.IsRealNode) {
            return node.BoundaryCurve.boundingBox.diagonal / 2;
        }
        else {
            const idealR = node.cachedIdealRadius;
            //TODO: which one?
            let r = this.metroGraphData.looseIntersections.GetMinimalDistanceToObstacles(node, node.Position, idealR);
            //const r = idealR;
            Assert.assert(r <= idealR);
            for (const adj of node.Neighbors)
                r = Math.min(r, node.Position.sub(adj.Position).length);
            return r;
        }
    }
    TryToGlueNodes(i, j, gluingMap, gluedDomain) {
        Assert.assert(i != j);
        const d = i.Position.sub(j.Position).length;
        const r1 = Math.max(i.Radius, 5);
        const r2 = Math.max(j.Radius, 5);
        if (d >= r1 + r2) {
            return;
        }
        // we are greedily trying to glue i to j
        if (!this.TryGlueOrdered(i, j, gluedDomain, gluingMap)) {
            this.TryGlueOrdered(j, i, gluedDomain, gluingMap);
        }
    }
    TryGlueOrdered(i, j, gluedDomain, gluingMap) {
        if (!gluingMap.has(i) && !gluedDomain.has(i) && this.NodeGluingIsAllowed(i, j, gluingMap)) {
            this.Map(i, j, gluedDomain, gluingMap);
            // TimeMeasurer.DebugOutput("gluing nodes " + i.serialNumber + " and " + j.serialNumber);
            return true;
        }
        return false;
    }
    Map(i, j, gluedDomain, gluingMap) {
        gluingMap.set(i, j);
        gluedDomain.add(j);
    }
    // trying to glue i to j
    NodeGluingIsAllowed(i, j, gluingMap) {
        for (const adj of i.Neighbors) {
            const k = NodePositionsAdjuster.Glued(adj, gluingMap);
            //1. check that we can merge these stations (== no intersections)
            const obstaclesToIgnore = this.metroGraphData.looseIntersections.ObstaclesToIgnoreForBundle(k, i);
            if (!this.metroGraphData.cdtIntersections.EdgeIsLegalSSPPS(k, j, k.Position, j.Position, obstaclesToIgnore))
                return false;
        }
        //2. check that cost of the routing is reduced
        const delta = this.ComputeCostDeltaAfterNodeGluing(i, j, gluingMap);
        if (delta < 0)
            return false;
        return true;
    }
    ComputeCostDeltaAfterNodeGluing(i, j, gluingMap) {
        const d = i.Position.sub(j.Position).length;
        if (i.Radius >= d || j.Radius >= d) {
            return 1;
        }
        let gain = 0;
        // ink
        const oldInk = this.metroGraphData.Ink;
        let newInk = this.metroGraphData.Ink - j.Position.sub(i.Position).length;
        for (const adj of i.Neighbors) {
            const k = NodePositionsAdjuster.Glued(adj, gluingMap);
            newInk -= k.Position.sub(i.Position).length;
            newInk += this.metroGraphData.RealEdgeCount(k, j) == 0 ? k.Position.sub(j.Position).length : 0;
        }
        gain += CostCalculator.InkError(oldInk, newInk, this.bundlingSettings);
        //path lengths
        for (const metroInfo of this.metroGraphData.MetroNodeInfosOfNode(i)) {
            const oldLength = metroInfo.Metroline.Length;
            let newLength = metroInfo.Metroline.Length;
            const pi = metroInfo.PolyPoint;
            const pa = pi.prev;
            const pb = pi.next;
            newLength -= pa.point.sub(i.Position).length + pb.point.sub(i.Position).length;
            newLength += pa.point.sub(j.Position).length + pb.point.sub(j.Position).length;
            gain += CostCalculator.PathLengthsError(oldLength, newLength, metroInfo.Metroline.IdealLength, this.bundlingSettings);
        }
        return gain;
    }
    RegenerateEdge(gluingMap, edgeIndex) {
        const poly = this.metroGraphData.Metrolines[edgeIndex].Polyline;
        if (!Array.from(poly).find((p) => gluingMap.has(this.metroGraphData.PointToStations.get(p)))) {
            return;
        }
        this.metroGraphData.Edges[edgeIndex].curve = Polyline.mkFromPoints(NodePositionsAdjuster.GluedPolyline(Array.from(poly).map((p) => this.metroGraphData.PointToStations.get(p)), gluingMap));
    }
    static GluedPolyline(metroline, gluedMap) {
        let i;
        const ret = new Stack();
        ret.push(metroline[0]);
        const seenStations = new Set();
        for (i = 1; i < metroline.length - 1; i++) {
            const station = NodePositionsAdjuster.Glued(metroline[i], gluedMap);
            if (seenStations.has(station)) {
                // we made a cycle - need to cut it out
                while (ret.top != station) {
                    seenStations.delete(ret.pop());
                }
                continue;
            }
            if (Point.closeDistEps(station.Position, ret.top.Position)) {
                continue;
            }
            seenStations.add(station);
            ret.push(station);
        }
        ret.push(metroline[i]);
        return Array.from(ret)
            .reverse()
            .map((n) => n.Position);
    }
    static Glued(i, gluedMap) {
        var _a;
        return (_a = gluedMap.get(i)) !== null && _a !== void 0 ? _a : i;
    }
    //  Unbundle unnecessary edges:
    //   instead of one bundle (a->bcd) we get two bundles (a->b,a->cd) with smaller ink
    UnglueEdgesFromBundleToSaveInk(alwaysExecuteSA) {
        const segsToPolylines = new PointPairMap();
        this.ink = this.metroGraphData.Ink;
        this.polylineLength = new Map();
        // create polylines
        for (const metroline of this.metroGraphData.Metrolines) {
            this.polylineLength.set(metroline, metroline.Length);
            for (let pp = metroline.Polyline.startPoint; pp.next != null; pp = pp.next) {
                const segment = new PointPair(pp.point, pp.next.point);
                addToPointPairMap(segsToPolylines, segment, metroline);
            }
        }
        const affectedPoints = new PointSet();
        let progress = false;
        for (const metroline of this.metroGraphData.Metrolines) {
            const obstaclesAllowedToIntersect = setIntersection(this.metroGraphData.PointToStations.get(metroline.Polyline.start).EnterableLoosePolylines, this.metroGraphData.PointToStations.get(metroline.Polyline.end).EnterableLoosePolylines);
            if (this.TrySeparateOnPolyline(metroline, segsToPolylines, affectedPoints, obstaclesAllowedToIntersect))
                progress = true;
        }
        if (progress)
            //TimeMeasurer.DebugOutput("unbundling");
            this.metroGraphData.Initialize(false);
        if (alwaysExecuteSA || progress)
            SimulatedAnnealing.FixRoutingMBP(this.metroGraphData, this.bundlingSettings, alwaysExecuteSA ? null : affectedPoints);
        return progress;
    }
    TrySeparateOnPolyline(metroline, segsToPolylines, affectedPoints, obstaclesAllowedToIntersect) {
        let progress = false;
        let relaxing = true;
        while (relaxing) {
            relaxing = false;
            for (let p = metroline.Polyline.startPoint; p.next != null && p.next.next != null; p = p.next) {
                if (this.TryShortcutPolypoint(p, segsToPolylines, affectedPoints, obstaclesAllowedToIntersect)) {
                    relaxing = true;
                }
            }
            if (relaxing) {
                progress = true;
            }
        }
        return progress;
    }
    TryShortcutPolypoint(pp, segsToPolylines, affectedPoints, obstaclesAllowedToIntersect) {
        if (this.SeparationShortcutAllowed(pp, segsToPolylines, obstaclesAllowedToIntersect)) {
            affectedPoints.add(pp.point);
            affectedPoints.add(pp.next.point);
            affectedPoints.add(pp.next.next.point);
            this.RemoveShortcuttedPolypoint(pp, segsToPolylines);
            return true;
        }
        return false;
    }
    //  allowed iff line (a,c) is legal and inkgain > 0
    SeparationShortcutAllowed(pp, segsToPolylines, obstaclesAllowedToIntersect) {
        const a = pp.point;
        const b = pp.next.point;
        const c = pp.next.next.point;
        const aStation = this.metroGraphData.PointToStations.get(a);
        const bStation = this.metroGraphData.PointToStations.get(b);
        const cStation = this.metroGraphData.PointToStations.get(c);
        // 1. intersections
        const set = uniteSets(setIntersectionOfArray([obstaclesAllowedToIntersect, bStation.EnterableLoosePolylines, aStation.EnterableLoosePolylines]), cStation.EnterableLoosePolylines);
        if (!this.metroGraphData.cdtIntersections.EdgeIsLegalSSPPS(aStation, cStation, a, c, set)) {
            return false;
        }
        // 2. cost gain
        const inkgain = this.GetInkgain(pp, segsToPolylines, a, b, c);
        if (inkgain < 0) {
            return false;
        }
        return true;
    }
    GetInkgain(pp, segsToPolylines, a, b, c) {
        //const abPolylines:Set<Metroline>, bcPolylines:Set<Metroline>, abcPolylines:Set<Metroline>;
        const [abPolylines, bcPolylines, abcPolylines] = this.FindPolylines(pp, segsToPolylines);
        let gain = 0;
        //ink
        const oldInk = this.ink;
        let newInk = this.ink;
        const ab = a.sub(b).length;
        const bc = b.sub(c).length;
        const ac = a.sub(c).length;
        if (abPolylines.size == abcPolylines.size)
            newInk -= ab;
        if (bcPolylines.size == abcPolylines.size)
            newInk -= bc;
        const t = segsToPolylines.get(new PointPair(a, c));
        if (!t || t.size == 0)
            newInk += ac;
        gain += CostCalculator.InkError(oldInk, newInk, this.bundlingSettings);
        //path lengths
        for (const metroline of abcPolylines) {
            const oldLength = this.polylineLength.get(metroline);
            const newLength = oldLength - (ab + bc - ac);
            gain += CostCalculator.PathLengthsError(oldLength, newLength, metroline.IdealLength, this.bundlingSettings);
        }
        //radii
        let nowR = this.GetCurrentHubRadius(this.metroGraphData.PointToStations.get(a));
        const widthABC = this.metroGraphData.GetWidthAN(Array.from(abcPolylines), this.bundlingSettings.EdgeSeparation);
        const widthABD = this.metroGraphData.GetWidthAN(Array.from(substractSets(abPolylines, abcPolylines)), this.bundlingSettings.EdgeSeparation);
        let idealR = HubRadiiCalculator.GetMinRadiusForTwoAdjacentBundlesNPPPNNB(nowR, a, c, b, widthABC, widthABD, this.bundlingSettings);
        if (idealR > nowR) {
            gain -= CostCalculator.RError(idealR, nowR, this.bundlingSettings);
        }
        //check opposite side
        nowR = this.GetCurrentHubRadius(this.metroGraphData.PointToStations.get(c));
        const widthCBD = this.metroGraphData.GetWidthAN(Array.from(substractSets(bcPolylines, abcPolylines)), this.bundlingSettings.EdgeSeparation);
        idealR = HubRadiiCalculator.GetMinRadiusForTwoAdjacentBundlesNPPPNNB(nowR, c, b, a, widthCBD, widthABC, this.bundlingSettings);
        if (idealR > nowR) {
            gain -= CostCalculator.RError(idealR, nowR, this.bundlingSettings);
        }
        return gain;
    }
    RemoveShortcuttedPolypoint(pp, segsToPolylines) {
        const a = pp.point;
        const b = pp.next.point;
        const c = pp.next.next.point;
        const [abPolylines, bcPolylines, abcPolylines] = this.FindPolylines(pp, segsToPolylines);
        const ab = distPP(a, b);
        const bc = distPP(b, c);
        const ac = distPP(a, c);
        //fixing ink
        if (abPolylines.size == abcPolylines.size)
            this.ink -= ab;
        if (bcPolylines.size == abcPolylines.size)
            this.ink -= bc;
        const t = segsToPolylines.get(new PointPair(a, c));
        if (!t || t.size == 0)
            this.ink += ac;
        //fixing edge lengths
        for (const metroline of abcPolylines) {
            const l = this.polylineLength.get(metroline);
            this.polylineLength.set(metroline, l - (ab + bc - ac));
        }
        //fixing polylines
        for (const metroline of abcPolylines) {
            const pp = Array.from(metroline.Polyline.polylinePoints()).find((p) => p.point.equal(b));
            this.RemovePolypoint(pp);
            removeFromPointPairMapTuple(segsToPolylines, [a, b], metroline);
            removeFromPointPairMapTuple(segsToPolylines, [b, c], metroline);
            addToPointMapTuple(segsToPolylines, [a, c], metroline);
        }
    }
    FindPolylines(pp, segsToPolylines) {
        const a = pp.point;
        const b = pp.next.point;
        const c = pp.next.next.point;
        const abPolylines = segsToPolylines.get_(a, b);
        const bcPolylines = segsToPolylines.get_(b, c);
        const abcPolylines = setIntersection(abPolylines, bcPolylines);
        return [abPolylines, bcPolylines, abcPolylines];
    }
    RemovePolypoint(p) {
        const prev = p.prev;
        const next = p.next;
        prev.next = next;
        next.prev = prev;
    }
    //  Fix the situation where a station has two neighbors that are almost in the same directions
    GlueCollinearNeighbors(step) {
        const affectedPoints = new PointSet();
        let progress = false;
        for (const node of this.metroGraphData.Stations)
            if (this.GlueCollinearNeighborsSPN(node, affectedPoints, step)) {
                progress = true;
            }
        if (progress) {
            // TimeMeasurer.DebugOutput("gluing edges");
            this.metroGraphData.Initialize(false);
            SimulatedAnnealing.FixRoutingMBP(this.metroGraphData, this.bundlingSettings, affectedPoints);
        }
        return progress;
    }
    GlueCollinearNeighborsSPN(node, affectedPoints, step) {
        if (node.Neighbors.length <= 1) {
            return false;
        }
        // node,adj => new via point
        const gluedEdges = new TupleMap();
        const neighbors = node.Neighbors;
        for (let i = 0; i < neighbors.length; i++) {
            this.TryToGlueEdges(node, neighbors[i], neighbors[(i + 1) % neighbors.length], gluedEdges, step);
        }
        if (gluedEdges.isEmpty) {
            return false;
        }
        for (const keyValueTriple of gluedEdges) {
            this.GlueEdge(keyValueTriple);
            affectedPoints.add(keyValueTriple[0].Position);
            affectedPoints.add(keyValueTriple[1].Position);
            affectedPoints.add(keyValueTriple[2]);
        }
        return true;
    }
    TryToGlueEdges(node, a, b, gluedEdges, step) {
        Assert.assert(a != b);
        const angle = Point.anglePCP(a.Position, node.Position, b.Position);
        if (angle < this.bundlingSettings.AngleThreshold) {
            const la = distPP(a.Position, node.Position);
            const lb = distPP(b.Position, node.Position);
            const ratio = Math.min(la, lb) / Math.max(la, lb);
            if (ratio < 0.05) {
                return;
            }
            if (la < lb) {
                if (this.EdgeGluingIsAllowedSSS(node, a, b)) {
                    this.AddEdgeToGlue(node, b, a, a.Position, gluedEdges);
                    return;
                }
            }
            else if (this.EdgeGluingIsAllowedSSS(node, b, a)) {
                this.AddEdgeToGlue(node, a, b, b.Position, gluedEdges);
                return;
            }
            // TODO: need this???
            if (step < 5 && ratio > 0.5) {
                const newPosition = this.ConstructGluingPoint(node, a, b);
                if (this.EdgeGluingIsAllowedSSSP(node, a, b, newPosition)) {
                    this.AddEdgeToGlue(node, b, a, newPosition, gluedEdges);
                }
            }
        }
    }
    ConstructGluingPoint(node, a, b) {
        // temp
        const len = Math.min(distPP(a.Position, node.Position), distPP(b.Position, node.Position) / 2);
        const dir = a.Position.sub(node.Position).normalize().add(b.Position.sub(node.Position).normalize());
        return node.Position.add(dir.mul(len / 2));
    }
    EdgeGluingIsAllowedSSS(node, a, b) {
        // 0. can't pass through real nodes
        if (a.IsRealNode || b.IsRealNode) {
            return false;
        }
        // 1. check intersections)  Here we are bending the edge (node->b) to pass through a.Position.
        // We need to be sure that segments (node,a) and (a,b) intersect only obstacles enterable for the bundle (node, b)
        if (!this.metroGraphData.cdtIntersections.EdgeIsLegal(a, b, a.Position, b.Position)) {
            return false;
        }
        const enterableForEdgeNodeB = this.metroGraphData.looseIntersections.ObstaclesToIgnoreForBundle(node, b);
        const crossingsOfEdgeNodeA = InteractiveEdgeRouter.IntersectionsOfLineAndRectangleNodeOverPolylineLR(LineSegment.mkPP(node.Position, a.Position), this.metroGraphData.LooseTree);
        if (crossingsOfEdgeNodeA.find((ii) => !enterableForEdgeNodeB.has(ii.seg1))) {
            return false;
        }
        const crossingsOfEdgeab = InteractiveEdgeRouter.IntersectionsOfLineAndRectangleNodeOverPolylineLR(LineSegment.mkPP(a.Position, b.Position), this.metroGraphData.LooseTree);
        if (crossingsOfEdgeab.find((ii) => !enterableForEdgeNodeB.has(ii.seg1))) {
            return false;
        }
        // 2. check cost
        const delta = this.ComputeCostDeltaAfterEdgeGluing(node, a, b, a.Position);
        if (delta < 0) {
            return false;
        }
        return true;
    }
    EdgeGluingIsAllowedSSSP(node, a, b, gluingPoint) {
        // 0. can't pass through real nodes
        if (!this.metroGraphData.looseIntersections.HubAvoidsObstaclesPNS__(gluingPoint, 0, setIntersection(a.EnterableLoosePolylines, b.EnterableLoosePolylines))) {
            return false;
        }
        // 1. check intersections
        if (!this.metroGraphData.cdtIntersections.EdgeIsLegal(node, null, node.Position, gluingPoint)) {
            return false;
        }
        if (!this.metroGraphData.cdtIntersections.EdgeIsLegal(a, null, a.Position, gluingPoint)) {
            return false;
        }
        if (!this.metroGraphData.cdtIntersections.EdgeIsLegal(b, null, b.Position, gluingPoint)) {
            return false;
        }
        // 2. check cost
        const delta = this.ComputeCostDeltaAfterEdgeGluing(node, a, b, gluingPoint);
        if (delta < 0) {
            return false;
        }
        return true;
    }
    ComputeCostDeltaAfterEdgeGluing(node, a, b, newp) {
        let gain = 0;
        //ink
        const oldInk = this.metroGraphData.Ink;
        const newInk = this.metroGraphData.Ink -
            distPP(node.Position, b.Position) -
            distPP(node.Position, a.Position) +
            distPP(node.Position, newp) +
            distPP(newp, a.Position) +
            distPP(newp, b.Position);
        gain += CostCalculator.InkError(oldInk, newInk, this.bundlingSettings);
        //path lengths
        for (const metroline of this.metroGraphData.GetIjInfo(node, b).Metrolines) {
            const oldLength = metroline.Length;
            const newLength = metroline.Length - distPP(node.Position, b.Position) + distPP(node.Position, newp) + distPP(newp, b.Position);
            gain += CostCalculator.PathLengthsError(oldLength, newLength, metroline.IdealLength, this.bundlingSettings);
        }
        for (const metroline of this.metroGraphData.GetIjInfo(node, a).Metrolines) {
            const oldLength = metroline.Length;
            const newLength = metroline.Length - distPP(node.Position, a.Position) + distPP(node.Position, newp) + distPP(newp, a.Position);
            gain += CostCalculator.PathLengthsError(oldLength, newLength, metroline.IdealLength, this.bundlingSettings);
        }
        //also compute radii gain
        //let nowR = Math.Min(GetCurrentHubRadius(node), (node.Position - newp).Length);
        //let id2 = HubRadiiCalculator.CalculateIdealHubRadiusWithNeighbors(metroGraphData, bundlingSettings, node);
        const id2 = node.cachedIdealRadius;
        const nowR = this.GetCurrentHubRadius(node);
        const idealR = HubRadiiCalculator.GetMinRadiusForTwoAdjacentBundles(nowR, node, node.Position, a, b, this.metroGraphData, this.bundlingSettings);
        if (idealR > nowR) {
            gain += CostCalculator.RError(idealR, nowR, this.bundlingSettings);
        }
        if (id2 > distPP(node.Position, newp) && !node.IsRealNode) {
            gain -= CostCalculator.RError(id2, distPP(node.Position, newp), this.bundlingSettings);
        }
        return gain;
    }
    AddEdgeToGlue(node, b, a, newp, gluedEdges) {
        // same edge in the reverse direction
        if (gluedEdges.has(a, node)) {
            return;
        }
        if (gluedEdges.has(b, node)) {
            return;
        }
        if (gluedEdges.has(node, a)) {
            return;
        }
        if (gluedEdges.has(node, b)) {
            return;
        }
        gluedEdges.set(node, a, newp);
        gluedEdges.set(node, b, newp);
    }
    GlueEdge(keyValuePair) {
        const node = keyValuePair[0];
        const a = keyValuePair[1];
        const newp = keyValuePair[2];
        for (const polylinePoint of node.MetroNodeInfos.map((i) => i.PolyPoint)) {
            if (polylinePoint.next != null && polylinePoint.next.point == a.Position) {
                this.SplitPolylinePoint(polylinePoint, newp);
            }
            else if (polylinePoint.prev != null && polylinePoint.prev.point == a.Position) {
                this.SplitPolylinePoint(polylinePoint.prev, newp);
            }
        }
    }
    SplitPolylinePoint(node, pointToInsert) {
        if (node.point == pointToInsert || node.next.point == pointToInsert) {
            return;
        }
        const p = PolylinePoint.mkFromPoint(pointToInsert);
        p.polyline = node.polyline;
        p.next = node.next;
        p.prev = node;
        p.next.prev = p;
        p.prev.next = p;
    }
    //  split each edge that is too much constrained by the obstacles
    RelaxConstrainedEdges() {
        const affectedPoints = new PointSet();
        let progress = false;
        for (const edge of this.metroGraphData.VirtualEdges())
            if (this.RelaxConstrainedEdge(edge[0], edge[1], affectedPoints))
                progress = true;
        if (progress) {
            //TimeMeasurer.DebugOutput("relaxing constrained edges");
            this.metroGraphData.Initialize(false);
            SimulatedAnnealing.FixRoutingMBP(this.metroGraphData, this.bundlingSettings, affectedPoints);
        }
        return progress;
    }
    RelaxConstrainedEdge(a, b, affectedPoints) {
        //find conflicting obstacles
        const idealWidth = this.metroGraphData.GetWidthSSN(a, b, this.bundlingSettings.EdgeSeparation);
        const t = { closestDist: new Array() };
        this.metroGraphData.cdtIntersections.BundleAvoidsObstacles(a, b, a.Position, b.Position, (0.99 * idealWidth) / 2.0, t);
        // Assert.assert(res); //todo still unsolved
        const closestPoints = t.closestDist;
        if (closestPoints.length > 0) {
            //find closest obstacle
            let bestDist = -1;
            let bestPoint;
            for (const d of closestPoints) {
                //should not be too close
                const distToSegmentEnd = Math.min(distPP(a.Position, d[1]), distPP(b.Position, d[1]));
                const distAB = distPP(a.Position, b.Position);
                const ratio = distToSegmentEnd / distAB;
                if (ratio < 0.1)
                    continue;
                //choose the closest
                const dist = distPP(d[0], d[1]);
                if (bestDist == -1 || dist < bestDist) {
                    bestDist = dist;
                    bestPoint = d[1];
                }
            }
            if (bestDist == -1)
                return false;
            if (!this.metroGraphData.looseIntersections.HubAvoidsObstaclesPNS__(bestPoint, 0, setIntersection(a.EnterableLoosePolylines, b.EnterableLoosePolylines)))
                return false;
            affectedPoints.add(bestPoint);
            affectedPoints.add(a.Position);
            affectedPoints.add(b.Position);
            for (const metroline of this.metroGraphData.GetIjInfo(a, b).Metrolines) {
                let pp = null;
                //TODO: replace the cycle!
                for (const ppp of metroline.Polyline.polylinePoints())
                    if (ppp.point.equal(a.Position)) {
                        pp = ppp;
                        break;
                    }
                Assert.assert(pp != null);
                if (pp.next != null && pp.next.point == b.Position)
                    this.SplitPolylinePoint(pp, bestPoint);
                else
                    this.SplitPolylinePoint(pp.prev, bestPoint);
            }
            return true;
        }
        return false;
    }
    // switch flips
    RemoveDoublePathCrossings() {
        const progress = new PathFixer(this.metroGraphData, this.metroGraphData.PointIsAcceptableForEdge.bind(this)).run();
        if (progress) {
            this.metroGraphData.Initialize(false);
            SimulatedAnnealing.FixRouting(this.metroGraphData, this.bundlingSettings);
        }
        return progress;
    }
}
//# sourceMappingURL=NodePositionsAdjuster.js.map