diff --git a/src/lib/components/map/AirspaceMap.svelte b/src/lib/components/map/AirspaceMap.svelte
index 5ecb031..936e28e 100644
--- a/src/lib/components/map/AirspaceMap.svelte
+++ b/src/lib/components/map/AirspaceMap.svelte
@@ -488,27 +488,38 @@
async function renderDynamicGeoJson(layer: any, component: any) {
const geojsonData = component.geojson as GeoJSON.FeatureCollection;
- // Lines Layer
- const lineFeatures = geojsonData.features.filter(
- (f) => f.geometry.type === 'LineString' || f.geometry.type === 'MultiLineString'
+ // Get the component name (first word) for zoomed out view
+ const componentName = component.name?.split(' ')[0] || '';
+
+ // Find the point feature that matches the component name
+ const mainPoint = geojsonData.features.find(
+ (f) =>
+ f.geometry.type === 'Point' &&
+ f.properties?.text &&
+ (Array.isArray(f.properties.text) ? f.properties.text[0] : f.properties.text) ===
+ componentName
);
- if (lineFeatures.length > 0) {
- const lineLayer = L!.geoJSON(
- {
- type: 'FeatureCollection',
- features: lineFeatures
- },
- {
- style: {
- color: component.color,
- weight: component.settings?.weight ?? 1,
- opacity: component.settings?.opacity ?? 0.8,
- lineCap: component.settings?.lineCap ?? 'round',
- lineJoin: component.settings?.lineJoin ?? 'round'
- }
- }
- );
- lineLayer.addTo(layer!);
+
+ // Create the zoomed out component label if we found the main point
+ let componentLabel: L.Marker | null = null;
+ if (mainPoint && mainPoint.geometry.type === 'Point') {
+ const coords = mainPoint.geometry.coordinates;
+ componentLabel = L!.marker([coords[1], coords[0]], {
+ icon: L!.divIcon({
+ html: `
+
+ `,
+ className: 'airspace-marker-container',
+ iconSize: [120, 30],
+ iconAnchor: [60, 15]
+ }),
+ interactive: false,
+ zIndexOffset: 1000
+ });
}
// Points Layer
@@ -536,27 +547,104 @@
.addTo(layer!);
}
- // Polygons
- const polygonFeatures = geojsonData.features.filter((f) => f.geometry.type === 'Polygon');
- if (polygonFeatures.length > 0) {
+ // Labels Layer
+ const labelFeatures = geojsonData.features.filter(
+ (f) => f.geometry.type === 'Point' && f.properties?.text
+ );
+ if (labelFeatures.length > 0) {
+ const labelLayer = L!.layerGroup().addTo(layer);
+
L!
.geoJSON(
{
type: 'FeatureCollection',
- features: polygonFeatures
+ features: labelFeatures
},
{
- style: {
- color: component.color,
- weight: component.settings?.weight ?? 1,
- opacity: component.settings?.opacity ?? 0.8,
- fillOpacity: component.settings?.fillOpacity ?? 0.8,
- lineCap: component.settings?.lineCap ?? 'round',
- lineJoin: component.settings?.lineJoin ?? 'round'
+ pointToLayer: (feature, latlng) => {
+ const text = feature.properties?.text;
+ if (!text) return L!.marker(latlng); // Fallback
+
+ // Handle both single strings and arrays of strings
+ const textLines = Array.isArray(text) ? text : [text];
+ const waypoint = textLines[0];
+ const restrictions = textLines.slice(1).filter((line) => /\d+[AB]$/.test(line));
+
+ // Create the detailed label
+ const detailedLabelHtml = `
+
+
${waypoint}
+ ${restrictions
+ .map(
+ (restriction) =>
+ `
+ ${restriction}
+
`
+ )
+ .join('')}
+
+ `;
+
+ const detailedIcon = L!.divIcon({
+ html: detailedLabelHtml,
+ className: 'airspace-marker-container',
+ iconSize: [120, (restrictions.length + 1) * 26],
+ iconAnchor: [60, ((restrictions.length + 1) * 26) / 2]
+ });
+
+ const marker = L!.marker(latlng, {
+ icon: detailedIcon,
+ interactive: false,
+ zIndexOffset: Math.floor(latlng.lat * 1000)
+ });
+
+ return marker;
}
}
)
- .addTo(layer!);
+ .addTo(labelLayer);
+
+ // Handle zoom levels
+ const updateLabelVisibility = () => {
+ const zoom = map!.getZoom();
+ if (zoom >= 8.8) {
+ labelLayer.addTo(layer);
+ if (componentLabel) {
+ componentLabel.removeFrom(layer);
+ }
+ } else {
+ labelLayer.removeFrom(layer);
+ if (componentLabel) {
+ componentLabel.addTo(layer);
+ }
+ }
+ };
+
+ map!.on('zoomend', updateLabelVisibility);
+ updateLabelVisibility(); // Initial state
+ }
+
+ // Lines Layer
+ const lineFeatures = geojsonData.features.filter(
+ (f) => f.geometry.type === 'LineString' || f.geometry.type === 'MultiLineString'
+ );
+ if (lineFeatures.length > 0) {
+ const lineLayer = L!.geoJSON(
+ {
+ type: 'FeatureCollection',
+ features: lineFeatures
+ },
+ {
+ style: {
+ color: component.color,
+ weight: component.settings?.weight ?? 2,
+ opacity: component.settings?.opacity ?? 0.8,
+ lineCap: component.settings?.lineCap ?? 'round',
+ lineJoin: component.settings?.lineJoin ?? 'round'
+ }
+ }
+ );
+ lineLayer.addTo(layer!);
}
}
@@ -1111,4 +1199,60 @@
cursor: pointer;
pointer-events: auto !important;
}
+
+ :global(.airspace-marker-container) {
+ background: transparent !important;
+ border: none !important;
+ }
+
+ :global(.airspace-dot) {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.2);
+ }
+
+ :global(.airspace-label) {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 2px;
+ }
+
+ :global(.airspace-label-text) {
+ font-family:
+ ui-sans-serif,
+ system-ui,
+ -apple-system,
+ BlinkMacSystemFont,
+ 'Segoe UI',
+ Roboto,
+ 'Helvetica Neue',
+ Arial,
+ sans-serif;
+ white-space: nowrap;
+ color: white;
+ padding: 3px 6px;
+ border-radius: 4px;
+ box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.2);
+ backdrop-filter: blur(4px);
+ }
+
+ :global(.airspace-label-text.primary) {
+ font-size: 13px;
+ font-weight: 600;
+ letter-spacing: 0.5px;
+ padding: 4px 8px;
+ }
+
+ :global(.airspace-label-text.secondary) {
+ font-size: 11px;
+ font-weight: 500;
+ letter-spacing: 0.25px;
+ padding: 2px 6px;
+ }
+
+ :global(.dark .airspace-label-text) {
+ box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.4);
+ }