Skip to content

Commit 2d9ed38

Browse files
committed
Add label support to overlays
1 parent c924b57 commit 2d9ed38

File tree

1 file changed

+176
-32
lines changed

1 file changed

+176
-32
lines changed

src/lib/components/map/AirspaceMap.svelte

Lines changed: 176 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -488,27 +488,38 @@
488488
async function renderDynamicGeoJson(layer: any, component: any) {
489489
const geojsonData = component.geojson as GeoJSON.FeatureCollection;
490490
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
494501
);
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+
});
512523
}
513524
514525
// Points Layer
@@ -536,27 +547,104 @@
536547
.addTo(layer!);
537548
}
538549
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+
542557
L!
543558
.geoJSON(
544559
{
545560
type: 'FeatureCollection',
546-
features: polygonFeatures
561+
features: labelFeatures
547562
},
548563
{
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;
556602
}
557603
}
558604
)
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!);
560648
}
561649
}
562650
@@ -1111,4 +1199,60 @@
11111199
cursor: pointer;
11121200
pointer-events: auto !important;
11131201
}
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+
}
11141258
</style>

0 commit comments

Comments
 (0)