diff --git a/src/meshcore_hub/web/static/css/app.css b/src/meshcore_hub/web/static/css/app.css index af89427..edb0e0f 100644 --- a/src/meshcore_hub/web/static/css/app.css +++ b/src/meshcore_hub/web/static/css/app.css @@ -347,3 +347,19 @@ footer { .leaflet-marker-icon:hover { z-index: 10000 !important; } + +/* ========================================================================== + Node Header Hero Map Background + ========================================================================== */ + +#header-map { + height: 100%; + width: 100%; + z-index: 0; +} + +/* Ensure Leaflet elements stay within the map layer */ +#header-map .leaflet-pane, +#header-map .leaflet-control { + z-index: auto !important; +} diff --git a/src/meshcore_hub/web/static/js/map-node.js b/src/meshcore_hub/web/static/js/map-node.js index 3a55cf3..9104d53 100644 --- a/src/meshcore_hub/web/static/js/map-node.js +++ b/src/meshcore_hub/web/static/js/map-node.js @@ -10,6 +10,12 @@ * - name: Node display name (required) * - type: Node adv_type (optional) * - publicKey: Node public key (optional, for linking) + * - elementId: Map container element ID (default: 'node-map') + * - interactive: Enable map interactions (default: true) + * - zoom: Initial zoom level (default: 15) + * - showMarker: Show node marker (default: true) + * - offsetX: Horizontal position of node (0-1, default: 0.5 = center) + * - offsetY: Vertical position of node (0-1, default: 0.5 = center) */ (function() { @@ -26,54 +32,90 @@ var nodeLon = config.lon; var nodeName = config.name || 'Unnamed Node'; var nodeType = config.type || ''; + var elementId = config.elementId || 'node-map'; + var interactive = config.interactive !== false; // Default true + var zoomLevel = config.zoom || 15; + var showMarker = config.showMarker !== false; // Default true + var offsetX = typeof config.offsetX === 'number' ? config.offsetX : 0.5; // 0-1, default center + var offsetY = typeof config.offsetY === 'number' ? config.offsetY : 0.5; // 0-1, default center // Check if map container exists - var mapContainer = document.getElementById('node-map'); + var mapContainer = document.getElementById(elementId); if (!mapContainer) { return; } + // Build map options + var mapOptions = {}; + + // Disable interactions if non-interactive + if (!interactive) { + mapOptions.dragging = false; + mapOptions.touchZoom = false; + mapOptions.scrollWheelZoom = false; + mapOptions.doubleClickZoom = false; + mapOptions.boxZoom = false; + mapOptions.keyboard = false; + mapOptions.zoomControl = false; + mapOptions.attributionControl = false; + } + // Initialize map centered on the node's location - var map = L.map('node-map').setView([nodeLat, nodeLon], 15); + var map = L.map(elementId, mapOptions).setView([nodeLat, nodeLon], zoomLevel); + + // Apply offset to position node at specified location instead of center + // offsetX/Y of 0.5 = center (no pan), 0.33 = 1/3 from left/top + if (offsetX !== 0.5 || offsetY !== 0.5) { + var containerWidth = mapContainer.offsetWidth; + var containerHeight = mapContainer.offsetHeight; + // Pan amount: how far to move the map so node appears at offset position + // Positive X = pan right (node moves left), Positive Y = pan down (node moves up) + var panX = (0.5 - offsetX) * containerWidth; + var panY = (0.5 - offsetY) * containerHeight; + map.panBy([panX, panY], { animate: false }); + } // Add tile layer L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors' }).addTo(map); - /** - * Get emoji marker based on node type - */ - function getNodeEmoji(type) { - var normalizedType = type ? type.toLowerCase() : null; - if (normalizedType === 'chat') return '💬'; - if (normalizedType === 'repeater') return '📡'; - if (normalizedType === 'room') return '🪧'; - return '📍'; + // Only add marker if showMarker is true + if (showMarker) { + /** + * Get emoji marker based on node type + */ + function getNodeEmoji(type) { + var normalizedType = type ? type.toLowerCase() : null; + if (normalizedType === 'chat') return '💬'; + if (normalizedType === 'repeater') return '📡'; + if (normalizedType === 'room') return '🪧'; + return '📍'; + } + + // Create marker icon (just the emoji, no label) + var emoji = getNodeEmoji(nodeType); + var icon = L.divIcon({ + className: 'custom-div-icon', + html: '' + emoji + '', + iconSize: [32, 32], + iconAnchor: [16, 16] + }); + + // Add marker + var marker = L.marker([nodeLat, nodeLon], { icon: icon }).addTo(map); + + // Only add popup if map is interactive + if (interactive) { + var typeHtml = nodeType ? '
Type: ' + nodeType + '
' : ''; + var popupContent = 'Coordinates: ' + nodeLat.toFixed(4) + ', ' + nodeLon.toFixed(4) + '
' + + 'Type: ' + nodeType + '
' : ''; - var popupContent = 'Coordinates: ' + nodeLat.toFixed(4) + ', ' + nodeLon.toFixed(4) + '
' + - 'Scan to add as contact
+First seen: {{ node.first_seen[:19].replace('T', ' ') if node.first_seen else '-' }}
-Last seen: {{ node.last_seen[:19].replace('T', ' ') if node.last_seen else '-' }}
-{{ node.public_key }}
{{ node.public_key }}
- Scan to add as contact
-Coordinates: {{ ns_map.lat }}, {{ ns_map.lon }}
-| Key | -Value | -Type | -
|---|---|---|
| {{ tag.key }} | -{{ tag.value }} | -{{ tag.value_type or 'string' }} | -
No tags defined.
- {% endif %} - {% if admin_enabled and is_authenticated %} - - {% endif %} -| Time | -Data | -Received By | +Key | +Value | +Type |
|---|---|---|---|---|---|
| {{ tel.received_at[:19].replace('T', ' ') if tel.received_at else '-' }} | -- {% if tel.parsed_data %} - {{ tel.parsed_data | tojson }} - {% else %} - - - {% endif %} - | -
- {% if tel.received_by %}
-
- {% if tel.receiver_tag_name or tel.receiver_name %}
- {{ tel.receiver_tag_name or tel.receiver_name }}
- {{ tel.received_by[:16] }}...
- {% else %}
- {{ tel.received_by[:16] }}...
- {% endif %}
-
- {% else %}
- -
- {% endif %}
- |
+ {{ tag.key }} | +{{ tag.value }} | +{{ tag.value_type or 'string' }} |
No telemetry recorded.
+No tags defined.
+ {% endif %} + {% if admin_enabled and is_authenticated %} + {% endif %}