|
488 | 488 | async function renderDynamicGeoJson(layer: any, component: any) { |
489 | 489 | const geojsonData = component.geojson as GeoJSON.FeatureCollection; |
490 | 490 |
|
491 | | - // Lines Layer |
492 | | - const lineFeatures = geojsonData.features.filter( |
493 | | - (f) => f.geometry.type === 'LineString' || f.geometry.type === 'MultiLineString' |
| 491 | + // Get the component name (first word) for zoomed out view |
| 492 | + const componentName = component.name?.split(' ')[0] || ''; |
| 493 | +
|
| 494 | + // Find the point feature that matches the component name |
| 495 | + const mainPoint = geojsonData.features.find( |
| 496 | + (f) => |
| 497 | + f.geometry.type === 'Point' && |
| 498 | + f.properties?.text && |
| 499 | + (Array.isArray(f.properties.text) ? f.properties.text[0] : f.properties.text) === |
| 500 | + componentName |
494 | 501 | ); |
495 | | - if (lineFeatures.length > 0) { |
496 | | - const lineLayer = L!.geoJSON( |
497 | | - { |
498 | | - type: 'FeatureCollection', |
499 | | - features: lineFeatures |
500 | | - }, |
501 | | - { |
502 | | - style: { |
503 | | - color: component.color, |
504 | | - weight: component.settings?.weight ?? 1, |
505 | | - opacity: component.settings?.opacity ?? 0.8, |
506 | | - lineCap: component.settings?.lineCap ?? 'round', |
507 | | - lineJoin: component.settings?.lineJoin ?? 'round' |
508 | | - } |
509 | | - } |
510 | | - ); |
511 | | - lineLayer.addTo(layer!); |
| 502 | +
|
| 503 | + // Create the zoomed out component label if we found the main point |
| 504 | + let componentLabel: L.Marker | null = null; |
| 505 | + if (mainPoint && mainPoint.geometry.type === 'Point') { |
| 506 | + const coords = mainPoint.geometry.coordinates; |
| 507 | + componentLabel = L!.marker([coords[1], coords[0]], { |
| 508 | + icon: L!.divIcon({ |
| 509 | + html: ` |
| 510 | + <div class="airspace-label"> |
| 511 | + <div class="airspace-label-text primary" style="background-color: ${component.color}"> |
| 512 | + ${componentName} |
| 513 | + </div> |
| 514 | + </div> |
| 515 | + `, |
| 516 | + className: 'airspace-marker-container', |
| 517 | + iconSize: [120, 30], |
| 518 | + iconAnchor: [60, 15] |
| 519 | + }), |
| 520 | + interactive: false, |
| 521 | + zIndexOffset: 1000 |
| 522 | + }); |
512 | 523 | } |
513 | 524 |
|
514 | 525 | // Points Layer |
|
536 | 547 | .addTo(layer!); |
537 | 548 | } |
538 | 549 |
|
539 | | - // Polygons |
540 | | - const polygonFeatures = geojsonData.features.filter((f) => f.geometry.type === 'Polygon'); |
541 | | - if (polygonFeatures.length > 0) { |
| 550 | + // Labels Layer |
| 551 | + const labelFeatures = geojsonData.features.filter( |
| 552 | + (f) => f.geometry.type === 'Point' && f.properties?.text |
| 553 | + ); |
| 554 | + if (labelFeatures.length > 0) { |
| 555 | + const labelLayer = L!.layerGroup().addTo(layer); |
| 556 | +
|
542 | 557 | L! |
543 | 558 | .geoJSON( |
544 | 559 | { |
545 | 560 | type: 'FeatureCollection', |
546 | | - features: polygonFeatures |
| 561 | + features: labelFeatures |
547 | 562 | }, |
548 | 563 | { |
549 | | - style: { |
550 | | - color: component.color, |
551 | | - weight: component.settings?.weight ?? 1, |
552 | | - opacity: component.settings?.opacity ?? 0.8, |
553 | | - fillOpacity: component.settings?.fillOpacity ?? 0.8, |
554 | | - lineCap: component.settings?.lineCap ?? 'round', |
555 | | - lineJoin: component.settings?.lineJoin ?? 'round' |
| 564 | + pointToLayer: (feature, latlng) => { |
| 565 | + const text = feature.properties?.text; |
| 566 | + if (!text) return L!.marker(latlng); // Fallback |
| 567 | +
|
| 568 | + // Handle both single strings and arrays of strings |
| 569 | + const textLines = Array.isArray(text) ? text : [text]; |
| 570 | + const waypoint = textLines[0]; |
| 571 | + const restrictions = textLines.slice(1).filter((line) => /\d+[AB]$/.test(line)); |
| 572 | +
|
| 573 | + // Create the detailed label |
| 574 | + const detailedLabelHtml = ` |
| 575 | + <div class="airspace-label detailed-label"> |
| 576 | + <div class="airspace-label-text primary" style="background-color: ${component.color}">${waypoint}</div> |
| 577 | + ${restrictions |
| 578 | + .map( |
| 579 | + (restriction) => |
| 580 | + `<div class="airspace-label-text secondary" style="background-color: ${component.color}99"> |
| 581 | + ${restriction} |
| 582 | + </div>` |
| 583 | + ) |
| 584 | + .join('')} |
| 585 | + </div> |
| 586 | + `; |
| 587 | +
|
| 588 | + const detailedIcon = L!.divIcon({ |
| 589 | + html: detailedLabelHtml, |
| 590 | + className: 'airspace-marker-container', |
| 591 | + iconSize: [120, (restrictions.length + 1) * 26], |
| 592 | + iconAnchor: [60, ((restrictions.length + 1) * 26) / 2] |
| 593 | + }); |
| 594 | +
|
| 595 | + const marker = L!.marker(latlng, { |
| 596 | + icon: detailedIcon, |
| 597 | + interactive: false, |
| 598 | + zIndexOffset: Math.floor(latlng.lat * 1000) |
| 599 | + }); |
| 600 | +
|
| 601 | + return marker; |
556 | 602 | } |
557 | 603 | } |
558 | 604 | ) |
559 | | - .addTo(layer!); |
| 605 | + .addTo(labelLayer); |
| 606 | +
|
| 607 | + // Handle zoom levels |
| 608 | + const updateLabelVisibility = () => { |
| 609 | + const zoom = map!.getZoom(); |
| 610 | + if (zoom >= 8.8) { |
| 611 | + labelLayer.addTo(layer); |
| 612 | + if (componentLabel) { |
| 613 | + componentLabel.removeFrom(layer); |
| 614 | + } |
| 615 | + } else { |
| 616 | + labelLayer.removeFrom(layer); |
| 617 | + if (componentLabel) { |
| 618 | + componentLabel.addTo(layer); |
| 619 | + } |
| 620 | + } |
| 621 | + }; |
| 622 | +
|
| 623 | + map!.on('zoomend', updateLabelVisibility); |
| 624 | + updateLabelVisibility(); // Initial state |
| 625 | + } |
| 626 | +
|
| 627 | + // Lines Layer |
| 628 | + const lineFeatures = geojsonData.features.filter( |
| 629 | + (f) => f.geometry.type === 'LineString' || f.geometry.type === 'MultiLineString' |
| 630 | + ); |
| 631 | + if (lineFeatures.length > 0) { |
| 632 | + const lineLayer = L!.geoJSON( |
| 633 | + { |
| 634 | + type: 'FeatureCollection', |
| 635 | + features: lineFeatures |
| 636 | + }, |
| 637 | + { |
| 638 | + style: { |
| 639 | + color: component.color, |
| 640 | + weight: component.settings?.weight ?? 2, |
| 641 | + opacity: component.settings?.opacity ?? 0.8, |
| 642 | + lineCap: component.settings?.lineCap ?? 'round', |
| 643 | + lineJoin: component.settings?.lineJoin ?? 'round' |
| 644 | + } |
| 645 | + } |
| 646 | + ); |
| 647 | + lineLayer.addTo(layer!); |
560 | 648 | } |
561 | 649 | } |
562 | 650 |
|
|
1111 | 1199 | cursor: pointer; |
1112 | 1200 | pointer-events: auto !important; |
1113 | 1201 | } |
| 1202 | +
|
| 1203 | + :global(.airspace-marker-container) { |
| 1204 | + background: transparent !important; |
| 1205 | + border: none !important; |
| 1206 | + } |
| 1207 | +
|
| 1208 | + :global(.airspace-dot) { |
| 1209 | + width: 8px; |
| 1210 | + height: 8px; |
| 1211 | + border-radius: 50%; |
| 1212 | + box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.2); |
| 1213 | + } |
| 1214 | +
|
| 1215 | + :global(.airspace-label) { |
| 1216 | + display: flex; |
| 1217 | + flex-direction: column; |
| 1218 | + align-items: center; |
| 1219 | + gap: 2px; |
| 1220 | + } |
| 1221 | +
|
| 1222 | + :global(.airspace-label-text) { |
| 1223 | + font-family: |
| 1224 | + ui-sans-serif, |
| 1225 | + system-ui, |
| 1226 | + -apple-system, |
| 1227 | + BlinkMacSystemFont, |
| 1228 | + 'Segoe UI', |
| 1229 | + Roboto, |
| 1230 | + 'Helvetica Neue', |
| 1231 | + Arial, |
| 1232 | + sans-serif; |
| 1233 | + white-space: nowrap; |
| 1234 | + color: white; |
| 1235 | + padding: 3px 6px; |
| 1236 | + border-radius: 4px; |
| 1237 | + box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.2); |
| 1238 | + backdrop-filter: blur(4px); |
| 1239 | + } |
| 1240 | +
|
| 1241 | + :global(.airspace-label-text.primary) { |
| 1242 | + font-size: 13px; |
| 1243 | + font-weight: 600; |
| 1244 | + letter-spacing: 0.5px; |
| 1245 | + padding: 4px 8px; |
| 1246 | + } |
| 1247 | +
|
| 1248 | + :global(.airspace-label-text.secondary) { |
| 1249 | + font-size: 11px; |
| 1250 | + font-weight: 500; |
| 1251 | + letter-spacing: 0.25px; |
| 1252 | + padding: 2px 6px; |
| 1253 | + } |
| 1254 | +
|
| 1255 | + :global(.dark .airspace-label-text) { |
| 1256 | + box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.4); |
| 1257 | + } |
1114 | 1258 | </style> |
0 commit comments