place<T> method

  1. @override
List<CollisionPlacement> place<T>({
  1. required MapCamera cam,
  2. required Size viewport,
  3. required List<CollisionNode<T>> nodes,
  4. required CollisionOptions options,
  5. required SpatialHashGrid grid,
  6. required Map<Key, Offset> previousOffsets,
})
override

Implementation

@override
List<CollisionPlacement> place<T>({
  required MapCamera cam,
  required Size viewport,
  required List<CollisionNode<T>> nodes,
  required CollisionOptions options,
  required SpatialHashGrid grid,
  required Map<Key, Offset> previousOffsets,
}) {
  final view = Offset.zero & viewport;

  final nodesIn = nodes.toList()
    ..sort((a, b) {
      final pa = screenPriority(a, view.center, cam);
      final pb = screenPriority(b, view.center, cam);
      if (pa != pb) return pa - pb;
      return a.key.hashCode.compareTo(b.key.hashCode);
    });

  grid.clear();

  for (final n in nodesIn) {
    final ar = n.anchorRectPx;
    if (ar != null) {
      grid.add(
        ar.left - options.anchorPad,
        ar.top - options.anchorPad,
        ar.right + options.anchorPad,
        ar.bottom + options.anchorPad,
        category: CollisionLayerBits.anchor,
      );
    }
  }

  bool fitsBase({
    required Rect rect,
    required int mask,
    bool hysteresis = false,
  }) {
    final pad =
        hysteresis ? math.max(0.0, options.pad - hysteresisPx) : options.pad;

    final l = rect.left - pad;
    final t = rect.top - pad;
    final r = rect.right + pad;
    final b = rect.bottom + pad;

    if (r <= view.left ||
        l >= view.right ||
        b <= view.top ||
        t >= view.bottom) {
      return false;
    }

    if (!options.avoidCollisions) return true;
    return !grid.collides(l, t, r, b, mask: mask);
  }

  bool fitsIn(
    Rect cand,
    int mask,
    List<Rect> slots, {
    bool hysteresis = false,
  }) {
    return fitsBase(rect: cand, mask: mask, hysteresis: hysteresis) &&
        insideSlotsOrMostly(cand, slots, minCover: minCover);
  }

  int angleIndexFromOffset(Offset o) {
    if (o.distanceSquared < 1e-9) return 0;
    final a = math.atan2(o.dy, o.dx);
    final t = (a + math.pi) / (2 * math.pi);
    return (t * angleSteps).round() % angleSteps;
  }

  Iterable<int> angularOrder(int start) sync* {
    yield start;
    for (int d = 1; d < angleSteps; d++) {
      final cw = (start + d) % angleSteps;
      final ccw = (start - d) % angleSteps;
      yield cw;
      yield ccw < 0 ? ccw + angleSteps : ccw;
    }
  }

  Iterable<double> radialOrder(double baseRadius) sync* {
    yield baseRadius;
    if (radialSlack <= 0 || radialSteps <= 0) return;
    final step = radialSlack / radialSteps;
    for (int i = 1; i <= radialSteps; i++) {
      yield math.max(0.0, baseRadius - i * step);
      yield baseRadius + i * step;
    }
  }

  double orbitRadiusFor(Rect base, Rect anchorRect) {
    final halfW = (base.width + anchorRect.width) * 0.5;
    final halfH = (base.height + anchorRect.height) * 0.5;
    return math.max(halfW, halfH) + options.anchorPad;
  }

  final placements = <CollisionPlacement>[];
  final placedRects = <Rect>[];

  for (final n in nodesIn) {
    final size = n.knownSize ?? options.defaultSize;
    final alignment = n.alignment;
    final margin = n.margin ?? EdgeInsets.zero;
    final rotate = n.rotate;
    final mask = n.collisionMask;

    final anchorPx = cam.latLngToScreenOffset(n.anchor);
    final base = overlayRect(
      origin: anchorPx,
      size: size,
      alignment: alignment * -1,
      mapRotationRad: cam.rotationRad,
      rotate: rotate,
      margin: margin,
    );

    final anchorRect = n.anchorRectPx ??
        Rect.fromCenter(center: anchorPx, width: 1, height: 1);

    final prev = previousOffsets[n.key] ?? Offset.zero;

    var outward = anchorPx - view.center;
    if (outward.distanceSquared < 1e-9) outward = const Offset(1, 0);

    final startIndex = prev.distanceSquared > 1e-9
        ? angleIndexFromOffset(prev)
        : angleIndexFromOffset(outward);

    final baseRadius = orbitRadiusFor(base, anchorRect);
    final slots = computeSlots(
      anchorRect,
      options.anchorPad,
      math.max(options.maxRadius, baseRadius + radialSlack),
    );

    Offset? bestOffset;
    Rect? bestRect;
    double bestScore = double.infinity;

    Offset? softOffset;
    Rect? softRect;
    double softScore = double.infinity;

    void consider(
      Offset off,
      Rect cand, {
      required bool hard,
      required bool hysteresis,
    }) {
      final inSlots = insideSlotsOrMostly(cand, slots, minCover: minCover);
      if (!inSlots) return;

      final hardFits = fitsIn(cand, mask, slots, hysteresis: hysteresis);

      final connectorEnd = connectorEntryPoint(cand, anchorPx);
      final score = placementScore(
        candidate: off,
        previous: prev,
        outward: outward,
        connectorStart: anchorPx,
        connectorEnd: connectorEnd,
        candidateRect: cand,
        slots: slots,
        obstacles: placedRects,
        previousWeight: 1.1,
        originWeight: 0.0,
        directionWeight: 5.0,
        lengthWeight: 0.02,
        slotCenterWeight: 10.0,
        connectorIntersectWeight: 120.0,
        connectorProximityWeight: 18.0,
        connectorAvoidDistance: 8.0,
      );

      if (hard && hardFits) {
        if (score < bestScore) {
          bestScore = score;
          bestOffset = off;
          bestRect = cand;
        }
      } else if (!strictCollision) {
        var penalty = score;

        if (!hardFits) {
          penalty += 200.0;
        }

        final l = cand.left - options.pad;
        final t = cand.top - options.pad;
        final r = cand.right + options.pad;
        final b = cand.bottom + options.pad;

        if (options.avoidCollisions &&
            grid.collides(l, t, r, b, mask: mask)) {
          penalty += 400.0;
        }

        if (penalty < softScore) {
          softScore = penalty;
          softOffset = off;
          softRect = cand;
        }
      }
    }

    // 1) Reuse previous placement if still acceptable.
    if (prev.distanceSquared > 1e-9) {
      final prevRect = base.shift(prev);
      if (fitsIn(prevRect, mask, slots, hysteresis: true)) {
        bestOffset = prev;
        bestRect = prevRect;
      }
    }

    // 2) Otherwise search on a thin orbit.
    if (bestOffset == null) {
      final order = angularOrder(startIndex).toList();

      for (final radius in radialOrder(baseRadius)) {
        for (int rank = 0; rank < order.length; rank++) {
          final i = order[rank];
          final a = (2 * math.pi) * (i / angleSteps);
          final off = Offset(radius * math.cos(a), radius * math.sin(a));
          final cand = base.shift(off);

          consider(
            off,
            cand,
            hard: true,
            hysteresis: rank <= hysteresisAngleSteps,
          );

          if (!strictCollision) {
            consider(
              off,
              cand,
              hard: false,
              hysteresis: false,
            );
          }
        }
      }
    }

    final chosenOffset = bestOffset ??
        softOffset ??
        (() {
          final ref = prev.distanceSquared > 1e-9 ? prev : outward;
          final d = ref.distance;
          if (d <= 1e-9) return Offset(baseRadius, 0);
          return ref / d * baseRadius;
        })();

    final rect = bestRect ?? softRect ?? base.shift(chosenOffset);

    grid.add(
      rect.left - options.pad,
      rect.top - options.pad,
      rect.right + options.pad,
      rect.bottom + options.pad,
      category: n.category,
    );

    final c = rect.center;
    placements.add(CollisionPlacement(
      key: n.key,
      offsetPx: chosenOffset,
      centerPx: Offset(c.dx, c.dy),
    ));
    placedRects.add(rect);
  }

  return placements;
}