diff --git a/static/css/gsm_spy_dashboard.css b/static/css/gsm_spy_dashboard.css index c256a34..ef362f1 100644 --- a/static/css/gsm_spy_dashboard.css +++ b/static/css/gsm_spy_dashboard.css @@ -341,7 +341,100 @@ body { background: var(--bg-dark); } -/* Map markers */ +/* Map markers - Vector Icons */ +.gsm-marker { + background: transparent !important; + border: none !important; + position: relative; +} + +.gsm-marker svg { + display: block; + transition: filter 0.2s ease; +} + +/* Selection ring for selected towers */ +.selection-ring { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 40px; + height: 40px; + border: 2px solid rgba(255,255,255,0.6); + border-radius: 50%; + animation: selection-pulse 2s ease-in-out infinite; + pointer-events: none; +} + +@keyframes selection-pulse { + 0%, 100% { + transform: translate(-50%, -50%) scale(1); + opacity: 0.6; + } + 50% { + transform: translate(-50%, -50%) scale(1.3); + opacity: 0.2; + } +} + +/* Rogue tower pulse ring */ +.rogue-pulse-ring { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 30px; + height: 30px; + border: 2px solid var(--accent-red); + border-radius: 50%; + animation: rogue-pulse 1.5s ease-out infinite; + pointer-events: none; +} + +@keyframes rogue-pulse { + 0% { + transform: translate(-50%, -50%) scale(0.8); + opacity: 0.8; + } + 100% { + transform: translate(-50%, -50%) scale(2); + opacity: 0; + } +} + +/* Device marker animations */ +.gsm-device { + animation: device-fade-in 0.3s ease-out; +} + +@keyframes device-fade-in { + 0% { + opacity: 0; + transform: scale(0.5); + } + 100% { + opacity: 1; + transform: scale(1); + } +} + +.device-fade-out { + animation: device-fade-out 1s ease-out forwards; +} + +@keyframes device-fade-out { + 0% { + opacity: 1; + transform: scale(1); + } + 100% { + opacity: 0; + transform: scale(0.3); + } +} + +/* Legacy circle marker support (fallback) */ .tower-marker { width: 20px; height: 20px; diff --git a/templates/gsm_spy_dashboard.html b/templates/gsm_spy_dashboard.html index 00cc60a..4382547 100644 --- a/templates/gsm_spy_dashboard.html +++ b/templates/gsm_spy_dashboard.html @@ -1656,6 +1656,35 @@ }; } + // ============================================ + // GSM ICON DEFINITIONS + // ============================================ + const GSM_ICONS = { + tower: 'M12 2L11 3v5h2V3l-1-1zm-1 6v2H9v2h2v2H9v2h2v2H9v2h6v-2h-2v-2h2v-2h-2v-2h2v-2h-2V8h-2zm-3 4H6v8h2v-8zm8 0h-2v8h2v-8zM5 14H3v6h2v-6zm14 0h-2v6h2v-6z', + device: 'M7 2v20h10V2H7zm2 2h6v12H9V4zm0 14h6v2H9v-2z' + }; + + // Create marker icon with SVG + function createGSMMarkerIcon(iconType, color, isSelected = false, isRogue = false) { + const path = GSM_ICONS[iconType] || GSM_ICONS.tower; + const size = iconType === 'tower' ? 24 : 20; + const glowColor = isSelected ? 'rgba(255,255,255,0.9)' : color; + const glowSize = isSelected ? '8px' : (isRogue ? '6px' : '4px'); + const pulseRing = isRogue && !isSelected ? + '
' : ''; + const selectionRing = isSelected ? + '' : ''; + + return L.divIcon({ + className: `gsm-marker gsm-${iconType}${isSelected ? ' selected' : ''}${isRogue ? ' rogue' : ''}`, + html: `${pulseRing}${selectionRing}`, + iconSize: [size, size], + iconAnchor: [size/2, size/2] + }); + } + // ============================================ // TOWER HANDLING // ============================================ @@ -1671,16 +1700,14 @@ return; } + const color = data.rogue ? '#e25d5d' : '#38c180'; + const isSelected = key === selectedTowerKey; + // Create or update marker if (!towerMarkers[key]) { - // Create new marker - const marker = L.circleMarker([data.lat, data.lon], { - radius: 10, - fillColor: data.rogue ? '#e25d5d' : '#38c180', - color: data.rogue ? '#e25d5d' : '#38c180', - weight: 2, - opacity: 1, - fillOpacity: 0.3 + // Create new marker with vector icon + const marker = L.marker([data.lat, data.lon], { + icon: createGSMMarkerIcon('tower', color, isSelected, data.rogue) }); marker.on('click', function() { @@ -1706,12 +1733,8 @@ const marker = towerMarkers[key]; marker.setLatLng([data.lat, data.lon]); - // Update color if rogue status changed - const color = data.rogue ? '#e25d5d' : '#38c180'; - marker.setStyle({ - fillColor: color, - color: color - }); + // Update icon if rogue status or selection changed + marker.setIcon(createGSMMarkerIcon('tower', color, isSelected, data.rogue)); } // Update towers list @@ -1785,11 +1808,22 @@ } function selectTower(key) { + const prevSelected = selectedTowerKey; selectedTowerKey = key; const tower = towers[key]; if (!tower) return; + // Update marker icons for both previous and new selection + [prevSelected, key].forEach(towerKey => { + if (towerKey && towerMarkers[towerKey] && towers[towerKey]) { + const t = towers[towerKey]; + const color = t.rogue ? '#e25d5d' : '#38c180'; + const isSelected = towerKey === selectedTowerKey; + towerMarkers[towerKey].setIcon(createGSMMarkerIcon('tower', color, isSelected, t.rogue)); + } + }); + // Update selected tower panel const infoDiv = document.getElementById('selectedTowerInfo'); infoDiv.innerHTML = ` @@ -1864,15 +1898,9 @@ const key = data.imsi || data.tmsi || `device_${Date.now()}`; devices[key] = data; - // Create device "blip" marker - const marker = L.circleMarker([data.lat, data.lon], { - radius: 5, - fillColor: '#e25d5d', - color: '#e25d5d', - weight: 2, - opacity: 1, - fillOpacity: 0.8, - className: 'device-blip' + // Create device marker with vector icon + const marker = L.marker([data.lat, data.lon], { + icon: createGSMMarkerIcon('device', '#00d9ff', false, false) }); marker.bindPopup(` @@ -1884,7 +1912,17 @@ marker.addTo(gsmMap); deviceMarkers[key] = marker; - // Remove marker after 5 seconds (pulse animation duration) + // Fade out and remove marker after 4 seconds + setTimeout(() => { + if (deviceMarkers[key]) { + const iconElement = deviceMarkers[key].getElement(); + if (iconElement) { + iconElement.classList.add('device-fade-out'); + } + } + }, 4000); + + // Remove marker after fade completes setTimeout(() => { if (deviceMarkers[key]) { gsmMap.removeLayer(deviceMarkers[key]);