Spaces:
Running
Running
| <html lang="vi"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Minh Đức P2P - GIS Pro Edition</title> | |
| <!-- Leaflet Core --> | |
| <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" /> | |
| <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> | |
| <!-- PeerJS --> | |
| <script src="https://unpkg.com/peerjs@1.5.2/dist/peerjs.min.js"></script> | |
| <!-- 1. Locate --> | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet.locatecontrol/dist/L.Control.Locate.min.css" /> | |
| <script src="https://cdn.jsdelivr.net/npm/leaflet.locatecontrol/dist/L.Control.Locate.min.js"></script> | |
| <!-- 2. Fullscreen --> | |
| <script src='https://api.mapbox.com/mapbox.js/plugins/leaflet-fullscreen/v1.0.1/Leaflet.fullscreen.min.js'></script> | |
| <link href='https://api.mapbox.com/mapbox.js/plugins/leaflet-fullscreen/v1.0.1/leaflet.fullscreen.css' rel='stylesheet' /> | |
| <!-- 3. Geocoder --> | |
| <link rel="stylesheet" href="https://unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.css" /> | |
| <script src="https://unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.js"></script> | |
| <!-- 4. Measure --> | |
| <link rel="stylesheet" href="https://unpkg.com/leaflet-measure/dist/leaflet-measure.css" /> | |
| <script src="https://unpkg.com/leaflet-measure/dist/leaflet-measure.js"></script> | |
| <!-- 5. MiniMap --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet-minimap/3.6.1/Control.MiniMap.min.css" /> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet-minimap/3.6.1/Control.MiniMap.min.js"></script> | |
| <!-- 6. Sidebar-v2 --> | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet-sidebar-v2@3.2.3/css/leaflet-sidebar.min.css" /> | |
| <script src="https://cdn.jsdelivr.net/npm/leaflet-sidebar-v2@3.2.3/js/leaflet-sidebar.min.js"></script> | |
| <!-- 7. Leaflet.Draw --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css"/> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script> | |
| <!-- 8. MousePosition --> | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet-mouse-position@1.2.0/src/L.Control.MousePosition.min.css" /> | |
| <script src="https://cdn.jsdelivr.net/npm/leaflet-mouse-position@1.2.0/src/L.Control.MousePosition.min.js"></script> | |
| <!-- 9. EasyButton --> | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet-easybutton@2/src/easy-button.css"> | |
| <script src="https://cdn.jsdelivr.net/npm/leaflet-easybutton@2/src/easy-button.js"></script> | |
| <style> | |
| body, html { margin: 0; padding: 0; height: 100%; font-family: 'Segoe UI', sans-serif; } | |
| #map { height: 100vh; width: 100%; background: #f0f0f0; } | |
| /* Marker radar mượt mà */ | |
| .p2p-radar { | |
| background: #2ecc71; | |
| border: 2px solid #ffffff; | |
| border-radius: 50%; | |
| box-shadow: 0 0 15px rgba(46, 204, 113, 0.6); | |
| } | |
| .p2p-radar::before { | |
| content: ''; display: block; width: 300%; height: 300%; margin-left: -100%; margin-top: -100%; | |
| border-radius: 50%; background-color: rgba(46, 204, 113, 0.4); | |
| animation: radar-pulse 2s infinite ease-out; | |
| } | |
| @keyframes radar-pulse { 0% { transform: scale(0.1); opacity: 1; } 100% { transform: scale(1); opacity: 0; } } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="sidebar" class="leaflet-sidebar collapsed"> | |
| <div class="leaflet-sidebar-tabs"> | |
| <ul role="tablist"> | |
| <li><a href="#home" role="tab">🏠</a></li> | |
| <li><a href="#p2p" role="tab">🛰️</a></li> | |
| <li><a href="#tools" role="tab">🛠️</a></li> | |
| </ul> | |
| </div> | |
| <div class="leaflet-sidebar-content"> | |
| <div class="leaflet-sidebar-pane" id="home"> | |
| <h1 class="leaflet-sidebar-header">Minh Đức GIS System <span class="leaflet-sidebar-close">❌</span></h1> | |
| <p style="padding: 20px 0;"><b>Trạng thái:</b> <span style="color: green;">Online</span></p> | |
| <p>ID Peer của bạn: <br><strong id="peer-id-display">...</strong></p> | |
| </div> | |
| <div class="leaflet-sidebar-pane" id="p2p"> | |
| <h1 class="leaflet-sidebar-header">Kết nối P2P <span class="leaflet-sidebar-close">❌</span></h1> | |
| <div id="user-list-ui" style="padding: 10px 0;"></div> | |
| </div> | |
| <div class="leaflet-sidebar-pane" id="tools"> | |
| <h1 class="leaflet-sidebar-header">Công cụ chuyên sâu <span class="leaflet-sidebar-close">❌</span></h1> | |
| <p>Sử dụng thanh công cụ bên trái để vẽ vùng/đường đi và thanh bên phải để đo đạc.</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="map"></div> | |
| <script> | |
| // --- KHỞI TẠO BẢN ĐỒ (STANDARD SETTINGS) --- | |
| // Reverted to default settings to ensure stable loading | |
| const osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: 'OSM' }); | |
| const satellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { attribution: 'Esri' }); | |
| const terrain = L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', { attribution: 'Topo' }); | |
| const map = L.map('map', { | |
| center: [10.7626, 106.6602], | |
| zoom: 13, | |
| layers: [osm], | |
| fullscreenControl: true | |
| }); | |
| // --- CÁC CÔNG CỤ ĐI KÈM --- | |
| const sidebar = L.control.sidebar({ container: 'sidebar' }).addTo(map); | |
| const drawnItems = new L.FeatureGroup(); | |
| map.addLayer(drawnItems); | |
| const drawControl = new L.Control.Draw({ | |
| edit: { featureGroup: drawnItems }, | |
| draw: { polygon: true, polyline: true, rectangle: true, circle: true, marker: true } | |
| }); | |
| map.addControl(drawControl); | |
| map.on(L.Draw.Event.CREATED, (e) => { | |
| drawnItems.addLayer(e.layer); | |
| }); | |
| L.control.mousePosition({ position: 'bottomleft', separator: ' | ', lngFirst: true }).addTo(map); | |
| const measureControl = new L.Control.Measure({ | |
| position: 'topright', | |
| primaryLengthUnit: 'meters', | |
| localization: 'vi' | |
| }).addTo(map); | |
| const miniLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'); | |
| new L.Control.MiniMap(miniLayer, { toggleDisplay: true, position: 'bottomright' }).addTo(map); | |
| L.control.locate({ flyTo: true, keepCurrentZoomLevel: true }).addTo(map).start(); | |
| L.Control.geocoder({ defaultMarkGeocode: true }).addTo(map); | |
| L.control.layers({ "Đường phố": osm, "Vệ tinh": satellite, "Địa hình": terrain }, { "Vùng vẽ": drawnItems }, { collapsed: false }).addTo(map); | |
| L.easyButton('<span>🏠</span>', function(btn, map){ | |
| map.setView([10.7626, 106.6602], 13); | |
| }, 'Về trung tâm').addTo(map); | |
| L.control.scale({ metric: true, imperial: false }).addTo(map); | |
| // --- LOGIC KẾT NỐI P2P --- | |
| const GROUP_ID = "M_DUC_PRO_GIS_"; | |
| const MAX_PEERS = 15; | |
| const myNum = Math.floor(Math.random() * MAX_PEERS); | |
| const peer = new Peer(GROUP_ID + myNum); | |
| const remoteMarkers = {}; | |
| peer.on('open', (id) => { | |
| document.getElementById('peer-id-display').innerText = id; | |
| setInterval(() => { | |
| for (let i = 0; i < MAX_PEERS; i++) { | |
| if (i !== myNum) { | |
| const targetId = GROUP_ID + i; | |
| if (!peer.connections[targetId]) setupConn(peer.connect(targetId)); | |
| } | |
| } | |
| }, 5000); | |
| }); | |
| peer.on('connection', setupConn); | |
| function setupConn(conn) { | |
| conn.on('data', (data) => { if (data.lat) updateRemote(conn.peer, data); }); | |
| conn.on('close', () => { | |
| if (remoteMarkers[conn.peer]) { | |
| map.removeLayer(remoteMarkers[conn.peer]); | |
| delete remoteMarkers[conn.peer]; | |
| updateListUI(); | |
| } | |
| }); | |
| } | |
| function updateRemote(id, coords) { | |
| if (remoteMarkers[id]) { | |
| remoteMarkers[id].setLatLng([coords.lat, coords.lng]); | |
| } else { | |
| const icon = L.divIcon({ className: 'p2p-radar', iconSize: [12, 12] }); | |
| remoteMarkers[id] = L.marker([coords.lat, coords.lng], { icon: icon }) | |
| .addTo(map) | |
| .bindPopup(`👤 <b>${id}</b>`); | |
| updateListUI(); | |
| } | |
| } | |
| function updateListUI() { | |
| const ui = document.getElementById('user-list-ui'); | |
| ui.innerHTML = ""; | |
| Object.keys(remoteMarkers).forEach(id => { | |
| const div = document.createElement('div'); | |
| div.style.padding = "10px"; | |
| div.style.borderBottom = "1px solid #ddd"; | |
| div.style.cursor = "pointer"; | |
| div.innerHTML = `📡 <b>${id}</b>`; | |
| div.onclick = () => { map.flyTo(remoteMarkers[id].getLatLng(), 17); sidebar.close(); }; | |
| ui.appendChild(div); | |
| }); | |
| } | |
| map.on('locationfound', (e) => { | |
| const myData = { lat: e.latlng.lat, lng: e.latlng.lng }; | |
| Object.keys(peer.connections).forEach(targetId => { | |
| peer.connections[targetId].forEach(c => { if (c.open) c.send(myData); }); | |
| }); | |
| }); | |
| </script> | |
| </body> | |
| </html> |