import {Point} from '../../math/geometry/point' import {closeDistEps} from '../../utils/compare' import {VisibilityVertex} from '../visibility/VisibilityVertex' import {ScanSegment} from './ScanSegment' import {SsstRectilinearPath} from './SsstRectilinearPath' import {VertexEntry} from './VertexEntry' import {VisibilityVertexRectilinear} from './VisibilityVertexRectiline' export class MsmtRectilinearPath { private bendPenaltyAsAPercentageOfDistance: number = SsstRectilinearPath.DefaultBendPenaltyAsAPercentageOfDistance // Temporary for accumulating target entries. private currentPassTargetEntries: VertexEntry[] = new Array(4) public constructor(bendPenalty: number) { this.bendPenaltyAsAPercentageOfDistance = bendPenalty } // Get the lowest-cost path from one of one or more sources to one of one or more targets, without waypoints. // One or more source vertices // One or more target vertices // A single enumeration of path points. GetPath(sources: Array, targets: Array): Array { const t = {entry: this.GetPathStage(null, sources, null, targets)} return SsstRectilinearPath.RestorePathV(t) } // Route a single stage of a possibly multi-stage (due to waypoints) path. // The VertexEntry array that was in the source vertex if it was the target of a prior stage. // The enumeration of source vertices; must be only one if sourceVertexEntries is non-null. // The enumeration of target vertex entries; must be only one if targetVertexEntries is non-null. // The VertexEntry array that is in the target at the end of the stage. private GetPathStage( sourceVertexEntries: VertexEntry[], sources: Array, targetVertexEntries: VertexEntry[], targets: Array, ): VertexEntry { const ssstCalculator = new SsstRectilinearPath() const t: {bestEntry: VertexEntry; bestCost: number} = { bestEntry: null, // This contains the best (lowest) path cost after normalizing origins to the center of the sources // and targets. This is used to avoid selecting a vertex pair whose path has more bends than another pair of // vertices, but the bend penalty didn't total enough to offset the additional length between the "better" pair. // This also plays the role of an upper bound on the path length; if a path cost is greater than adjustedMinCost // then we stop exploring it, which saves considerable time after low-cost paths have been found. bestCost: Number.MAX_VALUE / ScanSegment.OverlappedWeight, } let bestPathCostRatio: number = Number.POSITIVE_INFINITY // Calculate the bend penalty multiplier. This is a percentage of the distance between the source and target, // so that we have the same relative importance if we have objects of about size 20 that are about 100 apart // as for objects of about size 200 that are about 1000 apart. const sourceCenter: Point = MsmtRectilinearPath.Barycenter(sources) const targetCenter: Point = MsmtRectilinearPath.Barycenter(targets) const distance = SsstRectilinearPath.ManhattanDistance(sourceCenter, targetCenter) ssstCalculator.BendsImportance = Math.max(0.001, distance * (this.bendPenaltyAsAPercentageOfDistance * 0.01)) // We'll normalize by adding (a proportion of) the distance (only; not bends) from the current endpoints to // their centers. This is similar to routeToCenter, but routing multiple paths like this means we'll always // get at least a tie for the best vertex pair, whereas routeToCenter can introduce extraneous bends // if the sources/targets are not collinear with the center (such as an E-R diagram). // interiorLengthAdjustment is a way to decrease the cost adjustment slightly to allow a bend if it saves moving // a certain proportion of the distance parallel to the object before turning to it. const interiorLengthAdjustment = ssstCalculator.LengthImportance // VertexEntries for the current pass of the current stage, if multistage. const tempTargetEntries = targetVertexEntries != null ? this.currentPassTargetEntries : null // Process closest pairs first, so we can skip longer ones (jump out of SsstRectilinear sooner, often immediately). // This means that we'll be consistent on tiebreaking for equal scores with differing bend counts (the shorter // path will win). In overlapped graphs the shortest path may have more higher-weight edges. const stPairs: Array<[VisibilityVertex, VisibilityVertex]> = [] for (const s of sources) for (const t of targets) stPairs.push([s, t]) stPairs.sort(([a, b], [c, d]) => md(a, b) - md(c, d)) for (const [sv, tv] of stPairs) { if (Point.closeDistEps(sv.point, tv.point)) { continue } const sourceCostAdjustment = mdP(sv, sourceCenter) * interiorLengthAdjustment const targetCostAdjustment = mdP(tv, targetCenter) * interiorLengthAdjustment let adjustedBestCost = t.bestCost if (targetVertexEntries != null) { for (let i = 0; i < tempTargetEntries.length; i++) { tempTargetEntries[i] = null } adjustedBestCost = ssstCalculator.MultistageAdjustedCostBound(t.bestCost) } const lastEntry = ssstCalculator.GetPathWithCost( sourceVertexEntries, sv, sourceCostAdjustment, tempTargetEntries, tv, targetCostAdjustment, adjustedBestCost, ) if (tempTargetEntries != null) { MsmtRectilinearPath.UpdateTargetEntriesForEachDirection(targetVertexEntries, tempTargetEntries, t) continue } // This is the final (or only) stage. Break ties by picking the lowest ratio of cost to ManhattanDistance between the endpoints. if (lastEntry == null) { continue } const costRatio = lastEntry.Cost / md(sv, tv) if (lastEntry.Cost < t.bestCost || (closeDistEps(lastEntry.Cost, t.bestCost) && costRatio < bestPathCostRatio)) { t.bestCost = lastEntry.Cost t.bestEntry = lastEntry bestPathCostRatio = lastEntry.Cost / md(sv, tv) } } return t.bestEntry function md(s: VisibilityVertex, t: VisibilityVertex): number { return SsstRectilinearPath.ManhattanDistance(s.point, t.point) } function mdP(s: VisibilityVertex, t: Point): number { return SsstRectilinearPath.ManhattanDistance(s.point, t) } } private static UpdateTargetEntriesForEachDirection( targetVertexEntries: VertexEntry[], tempTargetEntries: VertexEntry[], t: { bestCost: number bestEntry: VertexEntry }, ) { for (let ii = 0; ii < tempTargetEntries.length; ii++) { const tempEntry = tempTargetEntries[ii] if (tempEntry == null) { continue } if (targetVertexEntries[ii] == null || tempEntry.Cost < targetVertexEntries[ii].Cost) { targetVertexEntries[ii] = tempEntry if (tempEntry.Cost < t.bestCost) { // This does not have the ratio tiebreaker because the individual stage path is only used as a success indicator. t.bestCost = tempEntry.Cost t.bestEntry = tempEntry } } } return } private static Barycenter(vertices: Array): Point { let center = new Point(0, 0) for (const vertex of vertices) { center = center.add(vertex.point) } return center.div(vertices.length) } }