aur-openlayers 19.6.5 → 19.6.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,7 +10,8 @@ import DragPan from 'ol/interaction/DragPan';
10
10
  import Modify from 'ol/interaction/Modify';
11
11
  import * as Observable from 'ol/Observable';
12
12
  import { unByKey } from 'ol/Observable';
13
- import { LineString, MultiLineString, Point } from 'ol/geom';
13
+ import { LineString, MultiLineString, Point, Polygon } from 'ol/geom';
14
+ import { toLonLat } from 'ol/proj';
14
15
 
15
16
  class LibService {
16
17
  constructor() { }
@@ -2299,7 +2300,7 @@ const resolveMaybeFn = (value, args) => typeof value === 'function' ? value(...a
2299
2300
  * Extracts flat coordinate arrays from a feature's geometry.
2300
2301
  * Supports LineString and MultiLineString; returns empty array for others.
2301
2302
  */
2302
- function extractLineCoords(geometry) {
2303
+ function extractLineCoords$1(geometry) {
2303
2304
  if (geometry instanceof LineString) {
2304
2305
  return [geometry.getCoordinates()];
2305
2306
  }
@@ -2341,6 +2342,7 @@ class ArrowDecorationManager {
2341
2342
  parentLayer;
2342
2343
  parentApi;
2343
2344
  moveEndKey;
2345
+ visibilityKey;
2344
2346
  unsubCollection;
2345
2347
  unsubChanges;
2346
2348
  rafId = null;
@@ -2352,12 +2354,13 @@ class ArrowDecorationManager {
2352
2354
  const parentZ = this.parentLayer.getZIndex() ?? 0;
2353
2355
  this.layer = new VectorLayer({
2354
2356
  source: this.source,
2355
- zIndex: parentZ + 1,
2357
+ zIndex: parentZ + 2,
2356
2358
  });
2357
2359
  this.layer.set('id', `__decoration_arrows`);
2358
2360
  this.map.addLayer(this.layer);
2359
2361
  this.syncVisibility();
2360
2362
  this.syncOpacity();
2363
+ this.visibilityKey = this.parentLayer.on('change:visible', () => this.syncVisibility());
2361
2364
  this.moveEndKey = this.map.on('moveend', () => this.scheduleUpdate());
2362
2365
  this.unsubCollection = this.parentApi.onModelsCollectionChanged(() => this.scheduleUpdate());
2363
2366
  this.unsubChanges = this.parentApi.onModelsChanged?.(() => this.scheduleUpdate());
@@ -2392,7 +2395,7 @@ class ArrowDecorationManager {
2392
2395
  const geom = feature.getGeometry();
2393
2396
  if (!geom)
2394
2397
  return;
2395
- const lines = extractLineCoords(geom);
2398
+ const lines = extractLineCoords$1(geom);
2396
2399
  for (const coords of lines) {
2397
2400
  const arrows = generateArrowPoints(coords, interval, offsetRatio);
2398
2401
  allArrows.push(...arrows);
@@ -2422,6 +2425,230 @@ class ArrowDecorationManager {
2422
2425
  this.rafId = null;
2423
2426
  }
2424
2427
  unByKey(this.moveEndKey);
2428
+ unByKey(this.visibilityKey);
2429
+ this.unsubCollection();
2430
+ this.unsubChanges?.();
2431
+ this.map.removeLayer(this.layer);
2432
+ }
2433
+ }
2434
+
2435
+ /**
2436
+ * Compute projected buffer distance for a segment by converting meters
2437
+ * to EPSG:3857 units using the segment's average latitude.
2438
+ */
2439
+ function projectedDistance(ax, ay, bx, by, meters) {
2440
+ const midY = (ay + by) / 2;
2441
+ const [, lat] = toLonLat([0, midY]);
2442
+ const scale = Math.cos((lat * Math.PI) / 180);
2443
+ return scale > 1e-10 ? meters / scale : meters;
2444
+ }
2445
+ /**
2446
+ * Compute the unit normal of a segment (pointing left when walking from a→b).
2447
+ */
2448
+ function segmentNormal(ax, ay, bx, by) {
2449
+ const dx = bx - ax;
2450
+ const dy = by - ay;
2451
+ const len = Math.hypot(dx, dy);
2452
+ if (len === 0)
2453
+ return [0, 0];
2454
+ return [-dy / len, dx / len];
2455
+ }
2456
+ /**
2457
+ * Generate a semicircle of points around a center, from startAngle to startAngle + PI.
2458
+ */
2459
+ function semicircle(cx, cy, radius, startAngle, segments) {
2460
+ const points = [];
2461
+ for (let i = 0; i <= segments; i++) {
2462
+ const angle = startAngle + (Math.PI * i) / segments;
2463
+ points.push([cx + radius * Math.cos(angle), cy + radius * Math.sin(angle)]);
2464
+ }
2465
+ return points;
2466
+ }
2467
+ /**
2468
+ * Generate a closed polygon ring representing a buffer around a polyline.
2469
+ *
2470
+ * @param coords Line vertices in EPSG:3857.
2471
+ * @param distance Buffer width in meters (one side).
2472
+ * @param cap End cap style: 'round' or 'flat'.
2473
+ * @returns Closed ring (first == last point), or empty array if line is degenerate.
2474
+ */
2475
+ function generateBufferPolygon(coords, distance, cap) {
2476
+ if (coords.length < 2)
2477
+ return [];
2478
+ const n = coords.length;
2479
+ const left = [];
2480
+ const right = [];
2481
+ // Compute per-vertex offset using averaged normals at joins
2482
+ for (let i = 0; i < n; i++) {
2483
+ let nx, ny, dist;
2484
+ if (i === 0) {
2485
+ // First vertex — use first segment's normal
2486
+ dist = projectedDistance(coords[0][0], coords[0][1], coords[1][0], coords[1][1], distance);
2487
+ [nx, ny] = segmentNormal(coords[0][0], coords[0][1], coords[1][0], coords[1][1]);
2488
+ }
2489
+ else if (i === n - 1) {
2490
+ // Last vertex — use last segment's normal
2491
+ dist = projectedDistance(coords[n - 2][0], coords[n - 2][1], coords[n - 1][0], coords[n - 1][1], distance);
2492
+ [nx, ny] = segmentNormal(coords[n - 2][0], coords[n - 2][1], coords[n - 1][0], coords[n - 1][1]);
2493
+ }
2494
+ else {
2495
+ // Interior vertex — average normals of adjacent segments
2496
+ const dist1 = projectedDistance(coords[i - 1][0], coords[i - 1][1], coords[i][0], coords[i][1], distance);
2497
+ const dist2 = projectedDistance(coords[i][0], coords[i][1], coords[i + 1][0], coords[i + 1][1], distance);
2498
+ dist = (dist1 + dist2) / 2;
2499
+ const [n1x, n1y] = segmentNormal(coords[i - 1][0], coords[i - 1][1], coords[i][0], coords[i][1]);
2500
+ const [n2x, n2y] = segmentNormal(coords[i][0], coords[i][1], coords[i + 1][0], coords[i + 1][1]);
2501
+ nx = n1x + n2x;
2502
+ ny = n1y + n2y;
2503
+ const len = Math.hypot(nx, ny);
2504
+ if (len > 1e-10) {
2505
+ nx /= len;
2506
+ ny /= len;
2507
+ // Miter length factor: 1 / cos(half-angle between segments)
2508
+ const dot = n1x * nx + n1y * ny;
2509
+ const miterScale = dot > 0.5 ? 1 / dot : 2; // Clamp miter at ratio 2 to prevent spikes
2510
+ dist *= miterScale;
2511
+ }
2512
+ else {
2513
+ [nx, ny] = segmentNormal(coords[i - 1][0], coords[i - 1][1], coords[i][0], coords[i][1]);
2514
+ }
2515
+ }
2516
+ const x = coords[i][0];
2517
+ const y = coords[i][1];
2518
+ left.push([x + nx * dist, y + ny * dist]);
2519
+ right.push([x - nx * dist, y - ny * dist]);
2520
+ }
2521
+ // Assemble the polygon ring: left forward → end cap → right backward → start cap → close
2522
+ const ring = [];
2523
+ // Left side (forward)
2524
+ for (let i = 0; i < n; i++) {
2525
+ ring.push(left[i]);
2526
+ }
2527
+ // End cap
2528
+ if (cap === 'round') {
2529
+ const [n2x, n2y] = segmentNormal(coords[n - 2][0], coords[n - 2][1], coords[n - 1][0], coords[n - 1][1]);
2530
+ const endAngle = Math.atan2(-n2y, -n2x);
2531
+ const dist = projectedDistance(coords[n - 2][0], coords[n - 2][1], coords[n - 1][0], coords[n - 1][1], distance);
2532
+ ring.push(...semicircle(coords[n - 1][0], coords[n - 1][1], dist, endAngle, 8));
2533
+ }
2534
+ else {
2535
+ ring.push(right[n - 1]);
2536
+ }
2537
+ // Right side (backward)
2538
+ for (let i = n - 2; i >= 0; i--) {
2539
+ ring.push(right[i]);
2540
+ }
2541
+ // Start cap
2542
+ if (cap === 'round') {
2543
+ const [n1x, n1y] = segmentNormal(coords[0][0], coords[0][1], coords[1][0], coords[1][1]);
2544
+ const startAngle = Math.atan2(n1y, n1x);
2545
+ const dist = projectedDistance(coords[0][0], coords[0][1], coords[1][0], coords[1][1], distance);
2546
+ ring.push(...semicircle(coords[0][0], coords[0][1], dist, startAngle, 8));
2547
+ }
2548
+ // Close ring
2549
+ ring.push(ring[0].slice());
2550
+ return ring;
2551
+ }
2552
+
2553
+ /**
2554
+ * Extracts flat coordinate arrays from a feature's geometry.
2555
+ * Supports LineString and MultiLineString; returns empty array for others.
2556
+ */
2557
+ function extractLineCoords(geometry) {
2558
+ if (geometry instanceof LineString) {
2559
+ return [geometry.getCoordinates()];
2560
+ }
2561
+ if (geometry instanceof MultiLineString) {
2562
+ return geometry.getCoordinates();
2563
+ }
2564
+ return [];
2565
+ }
2566
+ class BufferDecorationManager {
2567
+ source = new VectorSource();
2568
+ layer;
2569
+ config;
2570
+ map;
2571
+ parentLayer;
2572
+ parentApi;
2573
+ moveEndKey;
2574
+ visibilityKey;
2575
+ unsubCollection;
2576
+ unsubChanges;
2577
+ rafId = null;
2578
+ constructor(options) {
2579
+ this.config = options.config;
2580
+ this.map = options.map;
2581
+ this.parentLayer = options.parentLayer;
2582
+ this.parentApi = options.parentApi;
2583
+ const parentZ = this.parentLayer.getZIndex() ?? 0;
2584
+ this.layer = new VectorLayer({
2585
+ source: this.source,
2586
+ zIndex: parentZ,
2587
+ });
2588
+ this.layer.set('id', '__decoration_buffer');
2589
+ this.map.addLayer(this.layer);
2590
+ this.syncVisibility();
2591
+ this.syncOpacity();
2592
+ this.visibilityKey = this.parentLayer.on('change:visible', () => this.syncVisibility());
2593
+ this.moveEndKey = this.map.on('moveend', () => this.scheduleUpdate());
2594
+ this.unsubCollection = this.parentApi.onModelsCollectionChanged(() => this.scheduleUpdate());
2595
+ this.unsubChanges = this.parentApi.onModelsChanged?.(() => this.scheduleUpdate());
2596
+ }
2597
+ scheduleUpdate() {
2598
+ if (this.rafId !== null)
2599
+ return;
2600
+ this.rafId = requestAnimationFrame(() => {
2601
+ this.rafId = null;
2602
+ this.rebuild();
2603
+ });
2604
+ }
2605
+ rebuild() {
2606
+ this.syncVisibility();
2607
+ this.syncOpacity();
2608
+ if (!this.parentLayer.getVisible()) {
2609
+ this.source.clear();
2610
+ return;
2611
+ }
2612
+ const parentSource = this.parentLayer.getSource();
2613
+ if (!parentSource) {
2614
+ this.source.clear();
2615
+ return;
2616
+ }
2617
+ const cap = this.config.cap ?? 'round';
2618
+ const style = this.config.style;
2619
+ const allFeatures = [];
2620
+ parentSource.getFeatures().forEach((feature) => {
2621
+ const geom = feature.getGeometry();
2622
+ if (!geom)
2623
+ return;
2624
+ const lines = extractLineCoords(geom);
2625
+ for (const coords of lines) {
2626
+ const ring = generateBufferPolygon(coords, this.config.distance, cap);
2627
+ if (ring.length === 0)
2628
+ continue;
2629
+ const f = new Feature({ geometry: new Polygon([ring]) });
2630
+ f.setStyle(Array.isArray(style) ? style : [style]);
2631
+ allFeatures.push(f);
2632
+ }
2633
+ });
2634
+ this.source.clear();
2635
+ if (allFeatures.length > 0) {
2636
+ this.source.addFeatures(allFeatures);
2637
+ }
2638
+ }
2639
+ syncVisibility() {
2640
+ this.layer.setVisible(this.parentLayer.getVisible());
2641
+ }
2642
+ syncOpacity() {
2643
+ this.layer.setOpacity(this.parentLayer.getOpacity());
2644
+ }
2645
+ dispose() {
2646
+ if (this.rafId !== null) {
2647
+ cancelAnimationFrame(this.rafId);
2648
+ this.rafId = null;
2649
+ }
2650
+ unByKey(this.moveEndKey);
2651
+ unByKey(this.visibilityKey);
2425
2652
  this.unsubCollection();
2426
2653
  this.unsubChanges?.();
2427
2654
  this.map.removeLayer(this.layer);
@@ -2484,6 +2711,16 @@ class LayerManager {
2484
2711
  this.layers[descriptor.id] = layer;
2485
2712
  this.apis[descriptor.id] = api;
2486
2713
  this.map.addLayer(layer);
2714
+ if (descriptor.feature.decorations?.buffer) {
2715
+ const bufferManager = new BufferDecorationManager({
2716
+ map: this.map,
2717
+ parentLayer: layer,
2718
+ parentApi: api,
2719
+ config: descriptor.feature.decorations.buffer,
2720
+ });
2721
+ this.decorationManagers.push(bufferManager);
2722
+ layer.setZIndex((descriptor.zIndex ?? 0) + 1);
2723
+ }
2487
2724
  if (descriptor.feature.decorations?.arrows) {
2488
2725
  const decorationManager = new ArrowDecorationManager({
2489
2726
  map: this.map,