Skip to content

chào bạn #8

@datdeptrai-lam

Description

@datdeptrai-lam
<title>Bản đồ tương tác chuyên nghiệp</title> <style> html, body, #map { margin: 0; padding: 0; height: 100%; width: 100%; overflow: hidden; } .toolbar { position: absolute; top: 10px; left: 10px; background: white; padding: 5px; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); display: flex; flex-wrap: wrap; gap: 4px; z-index: 1000; transition: all 0.3s ease; max-width: 300px; } .toolbar.collapsed { transform: translateX(-100%); } .toolbar-toggle { position: absolute; top: 0; right: -30px; width: 30px; height: 100%; background: #3b82f6; color: white; border-top-right-radius: 8px; border-bottom-right-radius: 8px; display: flex; align-items: center; justify-content: center; cursor: pointer; } .layer-controls { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; } .layer-controls label { font-size: 12px; display: flex; align-items: center; gap: 2px; } .sidebar { position: absolute; top: 80px; left: 10px; width: 220px; background: white; padding: 10px; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); z-index: 1000; transition: transform 0.3s ease; } .sidebar.collapsed { transform: translateX(-100%); } .coordinates { position: absolute; bottom: 10px; right: 10px; background: rgba(255, 255, 255, 0.9); padding: 5px 10px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-size: 12px; } .zoom-controls { position: absolute; top: 120px; right: 10px; display: flex; flex-direction: column; gap: 4px; z-index: 1000; } .overview-map { position: absolute; bottom: 10px; left: 10px; width: 150px; height: 120px; border: 1px solid #ccc; z-index: 1000; } .magnify { position: absolute; top: 10px; right: 10px; width: 120px; height: 120px; border: 2px solid #000; border-radius: 50%; overflow: hidden; z-index: 1000; } .legend { position: absolute; bottom: 140px; /* Đặt ngay trên overviewMap */ left: 10px; background: white; padding: 10px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); z-index: 1000; max-width: 150px; } .legend.collapsed { display: none; } .attributes-table { position: absolute; bottom: 10px; right: 10px; background: white; padding: 10px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); max-height: 200px; overflow-y: auto; z-index: 1000; font-size: 12px; max-width: 300px; /* Giới hạn chiều rộng để tránh tràn */ } .popup { position: absolute; background: white; padding: 10px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); z-index: 1000; max-width: 300px; font-size: 12px; } .tooltip { position: relative; } .tooltip:hover::after { content: attr(data-tooltip); position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.8); color: white; padding: 4px 8px; border-radius: 4px; white-space: nowrap; z-index: 1000; font-size: 10px; } .active-tool { background-color: #3b82f6 !important; } @media (max-width: 640px) { .toolbar { flex-direction: column; width: 50px; padding: 2px; } .toolbar.collapsed { transform: translateY(-100%); } .toolbar-toggle { top: -30px; right: 0; left: 0; border-top-left-radius: 8px; border-top-right-radius: 8px; border-bottom-right-radius: 0; } .sidebar { width: 80%; } .layer-controls { flex-direction: column; } .attributes-table { max-width: 150px; /* Thu nhỏ trên màn hình nhỏ */ right: 5px; bottom: 5px; } .legend { bottom: 135px; /* Điều chỉnh trên màn hình nhỏ */ left: 5px; max-width: 120px; } .overview-map { width: 120px; height: 100px; left: 5px; bottom: 5px; } } </style>
Hành Chính Tuyến Giao Thông PostGIS

Công cụ

OSM Stamen Satellite Chọn đối tượng...

Yêu thích

    Bảng ký hiệu

    Bảng ký hiệu Ẩn

    Dữ liệu

    <script src="https://cdn.jsdelivr.net/npm/ol@v9.2.4/dist/ol.js"></script> <script src="https://cdn.jsdelivr.net/npm/toastify-js@1.12.0/src/toastify.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/jspdf@2.5.1/dist/jspdf.umd.min.js"></script> <script> const { jsPDF } = window.jspdf; document.addEventListener("DOMContentLoaded", () => { // Utility function for toast notifications function showToast(message, type = 'info') { Toastify({ text: message, duration: 3000, gravity: 'top', position: 'right', style: { background: type === 'error' ? '#ef4444' : '#10b981', }, }).showToast(); }
      // Map layers with cache optimization
      const osmSource = new ol.source.OSM({
        cacheSize: 200,
        transition: 0
      });
      const osmLayer = new ol.layer.Tile({
        source: osmSource,
        visible: true,
        title: 'osm'
      });
    
      const stamenSource = new ol.source.XYZ({
        url: 'https://stamen-tiles.a.ssl.fastly.net/toner/{z}/{x}/{y}.png',
        cacheSize: 200,
        transition: 0
      });
      const stamenLayer = new ol.layer.Tile({
        source: stamenSource,
        visible: false,
        title: 'stamen'
      });
    
      const satelliteSource = new ol.source.XYZ({
        url: 'https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}',
        cacheSize: 200,
        transition: 0
      });
      const satelliteLayer = new ol.layer.Tile({
        source: satelliteSource,
        visible: false,
        title: 'satellite'
      });
    
      // WMS Layer from GeoServer
      const hanhChinhLayer = new ol.layer.Tile({
        source: new ol.source.TileWMS({
          url: 'http://localhost:8080/geoserver/datdeptrai/wms',
          params: {
            'LAYERS': 'datdeptrai:hc_tinh_new',
            'TILED': true,
            'VERSION': '1.1.0',
            'FORMAT': 'image/png',
            'TRANSPARENT': true,
            'SRS': 'EPSG:3857'
          },
          serverType: 'geoserver',
          crossOrigin: 'anonymous'
        }),
        visible: false,
        title: 'hanhChinh',
        zIndex: 99
      });
    
      // WFS Layer from GeoServer with maxFeatures limit
      const wfsSource = new ol.source.Vector({
        format: new ol.format.GeoJSON(),
        url: function(extent) {
          return 'http://localhost:8080/geoserver/datdeptrai/ows?service=WFS&' +
            'version=1.0.0&request=GetFeature&typeName=datdeptrai:hc_tinh_new&' +
            'maxFeatures=50&' +
            'outputFormat=application/json&srsname=EPSG:3857&' +
            'bbox=' + extent.join(',') + ',EPSG:3857';
        },
        strategy: ol.loadingstrategy.bbox
      });
    
      const wfsLayer = new ol.layer.Vector({
        source: wfsSource,
        style: new ol.style.Style({
          stroke: new ol.style.Stroke({
            color: 'blue',
            width: 2
          }),
          fill: new ol.style.Fill({
            color: 'rgba(0, 0, 255, 0.1)'
          })
        }),
        visible: false,
        zIndex: 100
      });
    
      // PostGIS Layer via GeoServer WMS
      const postGISLayer = new ol.layer.Tile({
        source: new ol.source.TileWMS({
          url: 'http://localhost:8080/geoserver/datdeptrai/wms',
          params: {
            'LAYERS': 'datdeptrai:postgis_layer',
            'TILED': true,
            'VERSION': '1.1.0',
            'FORMAT': 'image/png',
            'TRANSPARENT': true,
            'SRS': 'EPSG:3857'
          },
          serverType: 'geoserver',
          crossOrigin: 'anonymous'
        }),
        visible: false,
        zIndex: 98
      });
    
      // Road Layer with zoom-based visibility
      const roadLayer = new ol.layer.Vector({
        source: new ol.source.Vector({
          format: new ol.format.GeoJSON(),
          url: 'http://localhost:8080/geoserver/datdeptrai/ows?service=WFS&' +
            'version=1.0.0&request=GetFeature&typeName=datdeptrai:gis_osm_roads_free_1&' +
            'maxFeatures=100&' +
            'outputFormat=application/json&srsname=EPSG:3857'
        }),
        style: function(feature, resolution) {
          const zoom = map.getView().getZoom();
          const fclass = feature.get('fclass');
          let style;
          if (zoom < 5) {
            return null;
          } else if (zoom >= 5 && zoom < 12) {
            if (fclass === 'primary' || fclass === 'railway') {
              style = new ol.style.Style({
                stroke: new ol.style.Stroke({
                  color: fclass === 'primary' ? 'red' : 'black',
                  width: fclass === 'primary' ? 3 : 2
                })
              });
            }
          } else if (zoom >= 12) {
            const colors = {
              'primary': 'red',
              'secondary': 'orange',
              'tertiary': 'yellow',
              'railway': 'black',
              'default': 'gray'
            };
            const widths = {
              'primary': 3,
              'secondary': 2,
              'tertiary': 1.5,
              'railway': 2,
              'default': 1
            };
            style = new ol.style.Style({
              stroke: new ol.style.Stroke({
                color: colors[fclass] || colors['default'],
                width: widths[fclass] || widths['default']
              })
            });
          }
          return style;
        },
        visible: false,
        zIndex: 97
      });
    
      const baseLayers = [osmLayer, stamenLayer, satelliteLayer];
    
      // Vector layers for drawing and selection
      const vectorSource = new ol.source.Vector();
      const vectorLayer = new ol.layer.Vector({ source: vectorSource });
      const routeSource = new ol.source.Vector();
      const routeLayer = new ol.layer.Vector({ source: routeSource });
      const selectSource = new ol.source.Vector();
      const selectLayer = new ol.layer.Vector({ source: selectSource });
    
      // Map initialization with zoom limit
      const map = new ol.Map({
        target: 'map',
        layers: [...baseLayers, hanhChinhLayer, wfsLayer, postGISLayer, roadLayer, vectorLayer, routeLayer, selectLayer],
        view: new ol.View({
          center: ol.proj.fromLonLat([105.85, 21.02]),
          zoom: 13,
          minZoom: 2,
          maxZoom: 18
        })
      });
    
      // Add Scale Line control
      const scaleLineControl = new ol.control.ScaleLine({
        units: 'metric'
      });
      map.addControl(scaleLineControl);
    
      // Overview Map
      const overviewMapControl = new ol.control.OverviewMap({
        target: 'overviewMap',
        layers: [new ol.layer.Tile({
          source: new ol.source.OSM({
            cacheSize: 200,
            transition: 0
          })
        })],
        view: new ol.View({
          center: ol.proj.fromLonLat([105.85, 21.02]),
          zoom: 10
        }),
        collapsible: true
      });
      map.addControl(overviewMapControl);
    
      // Magnify (Lens)
      const magnifyMap = new ol.Map({
        target: 'magnify',
        layers: [new ol.layer.Tile({
          source: new ol.source.OSM({
            cacheSize: 200,
            transition: 0
          })
        })],
        view: new ol.View({
          center: map.getView().getCenter(),
          zoom: map.getView().getZoom() + 2
        }),
        interactions: [],
        controls: []
      });
      map.on('pointermove', function(evt) {
        magnifyMap.getView().setCenter(evt.coordinate);
      });
      map.getView().on('change:resolution', function() {
        magnifyMap.getView().setZoom(map.getView().getZoom() + 2);
      });
    
      // Layer switching via dropdown
      document.getElementById('layerSelect').addEventListener('change', function() {
        const selectedLayer = this.value;
        baseLayers.forEach(layer => {
          layer.setVisible(layer.get('title') === selectedLayer);
        });
        showToast(`Đã chuyển sang lớp: ${selectedLayer}`);
      });
    
      // Toggle Layers via checkbox
      document.getElementById('toggleHanhChinh').addEventListener('change', function() {
        hanhChinhLayer.setVisible(this.checked);
        wfsLayer.setVisible(this.checked);
        showToast(this.checked ? 'Đã bật lớp hành chính.' : 'Đã tắt lớp hành chính.');
      });
    
      document.getElementById('toggleRoads').addEventListener('change', function() {
        roadLayer.setVisible(this.checked);
        document.getElementById('legend').classList.toggle('collapsed', !this.checked);
        showToast(this.checked ? 'Đã bật lớp tuyến giao thông.' : 'Đã tắt lớp tuyến giao thông.');
      });
    
      document.getElementById('togglePostGIS').addEventListener('change', function() {
        postGISLayer.setVisible(this.checked);
        showToast(this.checked ? 'Đã bật lớp PostGIS.' : 'Đã tắt lớp PostGIS.');
        if (this.checked) loadAttributes();
      });
    
      // Load WFS features into combobox
      wfsSource.on('featuresloadend', function() {
        const select = document.getElementById('featureSelect');
        select.innerHTML = '<option value="">Chọn đối tượng...</option>';
        wfsSource.forEachFeature(feature => {
          const name = feature.get('name') || feature.getId();
          const option = document.createElement('option');
          option.value = feature.getId();
          option.textContent = name;
          select.appendChild(option);
        });
      });
    
      document.getElementById('featureSelect').addEventListener('change', function() {
        const featureId = this.value;
        if (featureId) {
          const feature = wfsSource.getFeatureById(featureId);
          if (feature) {
            const extent = feature.getGeometry().getExtent();
            map.getView().fit(extent, { duration: 0 });
            showToast(`Đã zoom đến: ${feature.get('name') || featureId}`);
          }
        }
      });
    
      // Load attributes from PostGIS layer dynamically
      async function loadAttributes() {
        try {
          const response = await fetchWithTimeout(
            'http://localhost:8080/geoserver/datdeptrai/ows?service=WFS&' +
            'version=1.0.0&request=GetFeature&typeName=datdeptrai:postgis_layer&' +
            'outputFormat=application/json',
            { timeout: 10000 }
          );
          const data = await response.json();
          const features = data.features || [];
          const header = document.getElementById('attributesHeader');
          const tbody = document.getElementById('attributesBody');
          header.innerHTML = '';
          tbody.innerHTML = '';
    
          if (features.length === 0) {
            showToast('Không có dữ liệu thuộc tính.', 'info');
            return;
          }
    
          const sampleFeature = features[0];
          const properties = Object.keys(sampleFeature.properties);
          let headerRow = '<tr>';
          properties.forEach(prop => {
            headerRow += `<th class="border px-1 py-0.5">${prop}</th>`;
          });
          headerRow += '</tr>';
          header.innerHTML = headerRow;
    
          features.forEach(feature => {
            const row = document.createElement('tr');
            let rowContent = '';
            properties.forEach(prop => {
              rowContent += `<td class="border px-1 py-0.5">${feature.properties[prop] || 'N/A'}</td>`;
            });
            row.innerHTML = rowContent;
            tbody.appendChild(row);
          });
        } catch (error) {
          console.error('Error loading attributes:', error);
          showToast('Lỗi khi tải dữ liệu thuộc tính: ' + error.message, 'error');
        }
      }
    
      // Utility function for fetch with timeout
      async function fetchWithTimeout(url, options = {}) {
        const { timeout = 8000 } = options;
        const controller = new AbortController();
        const id = setTimeout(() => controller.abort(), timeout);
        const response = await fetch(url, {
          ...options,
          signal: controller.signal
        });
        clearTimeout(id);
        if (!response.ok) {
          throw new Error(`HTTP error! Status: ${response.status}`);
        }
        return response;
      }
    
      // Legend for roads
      document.getElementById('legendGraphic').src =
        'http://localhost:8080/geoserver/datdeptrai/wms?REQUEST=GetLegendGraphic&' +
        'VERSION=1.1.0&FORMAT=image/png&LAYER=datdeptrai:gis_osm_roads_free_1';
      document.getElementById('toggleLegend').addEventListener('click', function() {
        const legend = document.getElementById('legend');
        legend.classList.toggle('collapsed');
        this.textContent = legend.classList.contains('collapsed') ? 'Hiện' : 'Ẩn';
      });
    
      // Geolocation
      document.getElementById('locateBtn').addEventListener('click', () => {
        navigator.geolocation.getCurrentPosition(
          pos => {
            const coords = ol.proj.fromLonLat([pos.coords.longitude, pos.coords.latitude]);
            map.getView().animate({ center: coords, zoom: 16, duration: 0 });
            showToast('Đã định vị vị trí của bạn!');
          },
          err => showToast('Không thể lấy vị trí: ' + err.message, 'error')
        );
      });
    
      // Search
      document.getElementById('searchBox').addEventListener('keypress', async function(e) {
        if (e.key === 'Enter') {
          try {
            const query = this.value.trim();
            if (!query) {
              showToast('Vui lòng nhập địa điểm.', 'error');
              return;
            }
            const res = await fetchWithTimeout(
              `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}`,
              { timeout: 10000 }
            );
            const data = await res.json();
            if (data.length > 0) {
              const [lon, lat] = [parseFloat(data[0].lon), parseFloat(data[0].lat)];
              map.getView().animate({ center: ol.proj.fromLonLat([lon, lat]), zoom: 16, duration: 0 });
              showToast(`Đã tìm thấy: ${data[0].display_name}`);
            } else {
              showToast('Không tìm thấy địa điểm.', 'error');
            }
          } catch (error) {
            console.error('Search error:', error);
            showToast('Lỗi khi tìm kiếm: ' + error.message, 'error');
          }
        }
      });
    
      // Draw tools for measurement
      let draw;
      let activeTool = null;
      function toggleActiveTool(buttonId) {
        if (activeTool) {
          document.getElementById(activeTool).classList.remove('active-tool');
        }
        if (activeTool === buttonId) {
          activeTool = null;
          if (draw) map.removeInteraction(draw);
          return false;
        }
        document.getElementById(buttonId).classList.add('active-tool');
        activeTool = buttonId;
        return true;
      }
    
      function startDraw(type) {
        if (!toggleActiveTool(type === 'Polygon' ? 'drawPolygon' : 'measureDistance')) return;
        if (draw) map.removeInteraction(draw);
        draw = new ol.interaction.Draw({
          source: vectorSource,
          type
        });
        draw.on('drawend', function(e) {
          const geom = e.feature.getGeometry();
          if (type === 'Polygon') {
            const area = ol.sphere.getArea(geom);
            showToast(`Diện tích: ${(area / 10000).toFixed(2)} ha`);
          } else {
            const length = ol.sphere.getLength(geom);
            showToast(`Khoảng cách: ${(length / 1000).toFixed(2)} km`);
          }
          map.removeInteraction(draw);
          toggleActiveTool(type === 'Polygon' ? 'drawPolygon' : 'measureDistance');
        });
        map.addInteraction(draw);
      }
    
      document.getElementById('drawPolygon').addEventListener('click', () => startDraw('Polygon'));
      document.getElementById('measureDistance').addEventListener('click', () => startDraw('LineString'));
    
      // Draw and Modify Features
      const drawStyles = {
        'Point': new ol.style.Style({
          image: new ol.style.Circle({
            radius: 5,
            fill: new ol.style.Fill({ color: 'red' }),
            stroke: new ol.style.Stroke({ color: 'black', width: 1 })
          })
        }),
        'LineString': new ol.style.Style({
          stroke: new ol.style.Stroke({
            color: 'green',
            width: 2
          })
        }),
        'Polygon': new ol.style.Style({
          stroke: new ol.style.Stroke({
            color: 'blue',
            width: 2
          }),
          fill: new ol.style.Fill({
            color: 'rgba(0, 0, 255, 0.1)'
          })
        })
      };
    
      function startDrawing(type) {
        const buttonId = `draw${type}`;
        if (!toggleActiveTool(buttonId)) return;
        if (draw) map.removeInteraction(draw);
        draw = new ol.interaction.Draw({
          source: vectorSource,
          type,
          style: drawStyles[type]
        });
        draw.on('drawend', () => {
          toggleActiveTool(buttonId);
        });
        map.addInteraction(draw);
      }
    
      document.getElementById('drawPoint').addEventListener('click', () => startDrawing('Point'));
      document.getElementById('drawLine').addEventListener('click', () => startDrawing('LineString'));
      document.getElementById('drawPoly').addEventListener('click', () => startDrawing('Polygon'));
    
      const modify = new ol.interaction.Modify({ source: vectorSource });
      document.getElementById('modifyFeature').addEventListener('click', () => {
        if (!toggleActiveTool('modifyFeature')) return;
        map.addInteraction(modify);
        showToast('Chọn đối tượng để chỉnh sửa.');
      });
    
      // Delete Feature
      document.getElementById('deleteFeature').addEventListener('click', () => {
        if (!toggleActiveTool('deleteFeature')) return;
        showToast('Click vào đối tượng để xóa.');
        map.once('click', evt => {
          const feature = map.forEachFeatureAtPixel(map.getPixelFromCoordinate(evt.coordinate), (f, layer) => {
            if (layer === vectorLayer) return f;
          });
          if (feature) {
            vectorSource.removeFeature(feature);
            showToast('Đã xóa đối tượng.');
          } else {
            showToast('Không tìm thấy đối tượng để xóa.', 'error');
          }
          toggleActiveTool('deleteFeature');
        });
      });
    
      // Save Feature to GeoServer via WFS-T
      document.getElementById('saveFeature').addEventListener('click', async () => {
        const features = vectorSource.getFeatures();
        if (features.length === 0) {
          showToast('Không có đối tượng để lưu.', 'error');
          return;
        }
        const formatWFS = new ol.format.WFS();
        const featureNS = 'http://localhost:8080/datdeptrai';
        const featureType = 'datdeptrai:drawn_features';
        const node = formatWFS.writeTransaction(features, [], [], {
          featureNS: featureNS,
          featurePrefix: 'datdeptrai',
          featureType: 'drawn_features',
          srsName: 'EPSG:3857'
        });
        const serializer = new XMLSerializer();
        const transaction = serializer.serializeToString(node);
        try {
          const response = await fetch('http://localhost:8080/geoserver/wfs', {
            method: 'POST',
            headers: { 'Content-Type': 'text/xml' },
            body: transaction
          });
          if (response.ok) {
            showToast('Đã lưu đối tượng vào GeoServer.');
            vectorSource.clear();
          } else {
            throw new Error('Lỗi khi lưu vào GeoServer');
          }
        } catch (error) {
          console.error('Save feature error:', error);
          showToast('Lỗi khi lưu đối tượng: ' + error.message, 'error');
        }
      });
    
      // Selection tools
      let selectInteraction;
      function startSelection(type) {
        const buttonId = type === 'Box' ? 'selectBox' : 'selectPoint';
        if (!toggleActiveTool(buttonId)) return;
        if (selectInteraction) map.removeInteraction(selectInteraction);
        if (type === 'Box') {
          selectInteraction = new ol.interaction.DragBox({
            condition: ol.events.condition.platformModifierKeyOnly
          });
          selectInteraction.on('boxend', async function() {
            const extent = selectInteraction.getGeometry().getExtent();
            try {
              const response = await fetchWithTimeout(
                'http://localhost:8080/geoserver/datdeptrai/ows?service=WFS&' +
                'version=1.0.0&request=GetFeature&typeName=datdeptrai:hc_tinh_new&' +
                'outputFormat=application/json&srsname=EPSG:3857&' +
                'bbox=' + extent.join(',') + ',EPSG:3857',
                { timeout: 10000 }
              );
              const data = await response.json();
              selectSource.clear();
              const features = new ol.format.GeoJSON().readFeatures(data);
              selectSource.addFeatures(features);
              showToast(`Đã chọn ${features.length} đối tượng.`);
            } catch (error) {
              console.error('Selection error:', error);
              showToast('Lỗi khi chọn đối tượng: ' + error.message, 'error');
            }
            toggleActiveTool(buttonId);
          });
        } else if (type === 'Point') {
          selectInteraction = new ol.interaction.Select({
            condition: ol.events.condition.click,
            layers: [wfsLayer]
          });
          selectInteraction.on('select', function(e) {
            const selected = e.selected;
            if (selected.length > 0) {
              const feature = selected[0];
              showToast(`Đã chọn: ${feature.get('name') || feature.getId()}`);
            }
            toggleActiveTool(buttonId);
          });
        }
        map.addInteraction(selectInteraction);
      }
    
      document.getElementById('selectBox').addEventListener('click', () => startSelection('Box'));
      document.getElementById('selectPoint').addEventListener('click', () => startSelection('Point'));
    
      // Add Marker
      const icons = {
        default: 'https://openlayers.org/en/latest/examples/data/icon.png',
        home: 'https://img.icons8.com/color/24/000000/home.png',
        star: 'https://img.icons8.com/color/24/000000/star.png'
      };
      const popups = [];
    
      function sanitizeInput(input) {
        const div = document.createElement('div');
        div.textContent = input;
        return div.innerHTML;
      }
    
      function clearPopups() {
        popups.forEach(popup => map.removeOverlay(popup));
        popups.length = 0;
      }
    
      document.getElementById('addMarker').addEventListener('click', () => {
        if (!toggleActiveTool('addMarker')) return;
        showToast('Click trên bản đồ để thêm marker.');
        map.once('click', evt => {
          clearPopups();
          const coords = evt.coordinate;
          const lonLat = ol.proj.toLonLat(coords);
          const icon = sanitizeInput(prompt('Chọn icon (default, home, star):') || 'default');
          const name = sanitizeInput(prompt('Tên marker:') || 'Marker');
          const feature = new ol.Feature({
            geometry: new ol.geom.Point(coords),
            name,
            icon: icons[icon] || icons.default
          });
          feature.setStyle(
            new ol.style.Style({
              image: new ol.style.Icon({
                src: feature.get('icon'),
                scale: 0.1
              })
            })
          );
          vectorSource.addFeature(feature);
    
          const popup = new ol.Overlay({
            element: document.getElementById('popup'),
            positioning: 'bottom-center',
            offset: [0, -10]
          });
          popup.getElement().innerHTML = `<div>${name}</div>`;
          popup.setPosition(coords);
          map.addOverlay(popup);
          popups.push(popup);
    
          const favorites = JSON.parse(localStorage.getItem('favorites') || '[]');
          favorites.push({ name, lon: lonLat[0], lat: lonLat[1], icon });
          localStorage.setItem('favorites', JSON.stringify(favorites));
          updateFavorites();
          showToast(`Đã thêm marker: ${name}`);
          toggleActiveTool('addMarker');
        });
      });
    
      // Favorites
      function updateFavorites() {
        const favorites = JSON.parse(localStorage.getItem('favorites') || '[]');
        const list = document.getElementById('favoritesList');
        list.innerHTML = '';
        favorites.forEach((fav, index) => {
          const li = document.createElement('li');
          li.innerHTML = `
            ${fav.name} 
            <button class="text-blue-500 hover:underline text-xs" onclick="goToFavorite(${index})">👉</button>
            <button class="text-red-500 hover:underline text-xs" onclick="deleteFavorite(${index})">❌</button>
          `;
          list.appendChild(li);
        });
      }
    
      window.goToFavorite = index => {
        const favorites = JSON.parse(localStorage.getItem('favorites') || '[]');
        const fav = favorites[index];
        map.getView().animate({ center: ol.proj.fromLonLat([fav.lon, fav.lat]), zoom: 16, duration: 0 });
        showToast(`Đã di chuyển đến: ${fav.name}`);
      };
    
      window.deleteFavorite = index => {
        const favorites = JSON.parse(localStorage.getItem('favorites') || '[]');
        favorites.splice(index, 1);
        localStorage.setItem('favorites', JSON.stringify(favorites));
        updateFavorites();
        showToast('Đã xóa mục yêu thích.');
      };
    
      updateFavorites();
    
      // Routing
      let routePoints = [];
      document.getElementById('routeBtn').addEventListener('click', () => {
        if (!toggleActiveTool('routeBtn')) return;
        showToast('Click 2 điểm trên bản đồ để chỉ đường.');
        routePoints = [];
        routeSource.clear();
        map.on('click', onMapClick);
      });
    
      async function onMapClick(evt) {
        routePoints.push(ol.proj.toLonLat(evt.coordinate));
        if (routePoints.length === 2) {
          map.un('click', onMapClick);
          try {
            const [start, end] = routePoints;
            const res = await fetchWithTimeout(
              `http://router.project-osrm.org/route/v1/driving/${start[0]},${start[1]};${end[0]},${end[1]}?overview=full&geometries=geojson`,
              { timeout: 10000 }
            );
            const data = await res.json();
            if (data.routes && data.routes[0]) {
              const route = data.routes[0].geometry;
              const feature = new ol.Feature({
                geometry: new ol.geom.LineString(route.coordinates).transform('EPSG:4326', 'EPSG:3857')
              });
              feature.setStyle(
                new ol.style.Style({
                  stroke: new ol.style.Stroke({
                    color: '#ff0000',
                    width: 4
                  })
                })
              );
              routeSource.addFeature(feature);
              showToast(`Đã tạo tuyến đường: ${(data.routes[0].distance / 1000).toFixed(2)} km`);
            } else {
              showToast('Không tìm thấy tuyến đường.', 'error');
            }
          } catch (error) {
            console.error('Routing error:', error);
            showToast('Lỗi khi tạo tuyến đường: ' + error.message, 'error');
          }
          toggleActiveTool('routeBtn');
        }
      }
    
      // Export Map to Image
      document.getElementById('exportMap').addEventListener('click', () => {
        map.once('rendercomplete', () => {
          const mapCanvas = document.createElement('canvas');
          const size = map.getSize();
          mapCanvas.width = size[0];
          mapCanvas.height = size[1];
          const mapContext = mapCanvas.getContext('2d');
          Array.prototype.forEach.call(
            document.querySelectorAll('.ol-layer canvas'),
            canvas => {
              if (canvas.width > 0) {
                const opacity = canvas.parentNode.style.opacity;
                mapContext.globalAlpha = opacity === '' ? 1 : Number(opacity);
                const transform = canvas.style.transform;
                const matrix = transform.match(/^matrix\(([^\)]+)\)$/)[1].split(',').map(Number);
                mapContext.setTransform(matrix[0], matrix[1], matrix[2], matrix[3], matrix[4], matrix[5]);
                mapContext.drawImage(canvas, 0, 0);
              }
            }
          );
          const link = document.createElement('a');
          link.download = 'map_export.png';
          link.href = mapCanvas.toDataURL();
          link.click();
          showToast('Đã xuất bản đồ thành công!');
        });
        map.renderSync();
      });
    
      // Export Map to PDF with annotations
      document.getElementById('exportPDF').addEventListener('click', () => {
        map.once('rendercomplete', () => {
          const mapCanvas = document.createElement('canvas');
          const size = map.getSize();
          const scale = 2;
          mapCanvas.width = size[0] * scale;
          mapCanvas.height = size[1] * scale;
          const mapContext = mapCanvas.getContext('2d');
          Array.prototype.forEach.call(
            document.querySelectorAll('.ol-layer canvas'),
            canvas => {
              if (canvas.width > 0) {
                const opacity = canvas.parentNode.style.opacity;
                mapContext.globalAlpha = opacity === '' ? 1 : Number(opacity);
                const transform = canvas.style.transform;
                const matrix = transform.match(/^matrix\(([^\)]+)\)$/)[1].split(',').map(Number);
                mapContext.setTransform(
                  matrix[0] * scale, matrix[1] * scale,
                  matrix[2] * scale, matrix[3] * scale,
                  matrix[4] * scale, matrix[5] * scale
                );
                mapContext.drawImage(canvas, 0, 0, canvas.width * scale, canvas.height * scale);
              }
            }
          );
    
          const pdf = new jsPDF({
            orientation: 'landscape',
            unit: 'mm',
            format: 'a4'
          });
          const imgData = mapCanvas.toDataURL('image/png');
          const imgProps = pdf.getImageProperties(imgData);
          const pdfWidth = pdf.internal.pageSize.getWidth() - 20;
          const pdfHeight = (imgProps.height * pdfWidth) / imgProps.width;
          const margin = 10;
    
          pdf.setFontSize(16);
          pdf.text('Bản đồ WebGIS', margin, margin);
          pdf.setFontSize(10);
          pdf.text(`Ngày xuất: ${new Date().toLocaleString()}`, margin, margin + 10);
          pdf.text(`Tỷ lệ: 1:${Math.round(map.getView().getResolution() * 100000)}`, margin, margin + 15);
    
          pdf.addImage(imgData, 'PNG', margin, margin + 20, pdfWidth, pdfHeight);
          pdf.save('map_export.pdf');
          showToast('Đã xuất bản đồ ra PDF!');
        });
        map.renderSync();
      });
    
      // Full Screen
      document.getElementById('fullScreen').addEventListener('click', () => {
        const elem = document.documentElement;
        if (!document.fullscreenElement) {
          elem.requestFullscreen().then(() => showToast('Đã bật toàn màn hình.'));
        } else {
          document.exitFullscreen().then(() => showToast('Đã thoát toàn màn hình.'));
        }
      });
    
      // Zoom Controls
      document.getElementById('zoomIn').addEventListener('click', () => {
        map.getView().animate({ zoom: map.getView().getZoom() + 1, duration: 0 });
      });
      document.getElementById('zoomOut').addEventListener('click', () => {
        map.getView().animate({ zoom: map.getView().getZoom() - 1, duration: 0 });
      });
    
      // Coordinates
      map.on('pointermove', evt => {
        const lonLat = ol.proj.toLonLat(evt.coordinate);
        document.getElementById('coords').textContent = `Tọa độ: ${lonLat[0].toFixed(4)}, ${lonLat[1].toFixed(4)}`;
      });
    
      map.on('moveend', function() {
        const extent = map.getView().calculateExtent(map.getSize());
        const bottomLeft = ol.proj.toLonLat(ol.extent.getBottomLeft(extent));
        const topRight = ol.proj.toLonLat(ol.extent.getTopRight(extent));
        document.getElementById('coords').textContent += ` | Góc trái: ${bottomLeft[0].toFixed(4)}, ${bottomLeft[1].toFixed(4)} | Góc phải: ${topRight[0].toFixed(4)}, ${topRight[1].toFixed(4)}`;
      });
    
      // WMS GetFeatureInfo
      map.on('singleclick', async function(evt) {
        const view = map.getView();
        const resolution = view.getResolution();
        const coordinate = evt.coordinate;
    
        const layersToQuery = [
          { layer: hanhChinhLayer, name: 'hc_tinh_new' },
          { layer: roadLayer, name: 'gis_osm_roads_free_1' },
          { layer: postGISLayer, name: 'postgis_layer' }
        ];
    
        clearPopups();
        for (const layerInfo of layersToQuery) {
          if (layerInfo.layer.getVisible()) {
            const url = layerInfo.layer.getSource().getFeatureInfoUrl(
              coordinate,
              resolution,
              'EPSG:3857',
              { 'INFO_FORMAT': 'application/json' }
            );
            if (url) {
              try {
                const response = await fetchWithTimeout(url, { timeout: 10000 });
                const data = await response.json();
                if (data.features && data.features.length > 0) {
                  const feature = data.features[0];
                  const properties = feature.properties;
                  let content = '<div>';
                  for (const key in properties) {
                    content += `<b>${key}</b>: ${properties[key] || 'N/A'}<br>`;
                  }
                  content += '</div>';
                  const popup = new ol.Overlay({
                    element: document.getElementById('popup'),
                    positioning: 'bottom-center',
                    offset: [0, -10]
                  });
                  popup.getElement().innerHTML = content;
                  popup.setPosition(coordinate);
                  map.addOverlay(popup);
                  popups.push(popup);
                  break;
                }
              } catch (error) {
                console.error('GetFeatureInfo error:', error);
                showToast('Lỗi khi lấy thông tin: ' + error.message, 'error');
              }
            }
          }
        }
      });
    
      // Toolbar Toggle
      document.getElementById('toolbarToggle').addEventListener('click', () => {
        const toolbar = document.getElementById('toolbar');
        toolbar.classList.toggle('collapsed');
        document.getElementById('toolbarToggle').innerHTML = toolbar.classList.contains('collapsed') ? '<i class="fas fa-angle-right"></i>' : '<i class="fas fa-bars"></i>';
        showToast(toolbar.classList.contains('collapsed') ? 'Đã thu gọn thanh công cụ.' : 'Đã mở thanh công cụ.');
      });
    
      // Sidebar Toggle
      document.getElementById('toggleSidebar').addEventListener('click', () => {
        document.getElementById('sidebar').classList.toggle('collapsed');
        showToast(document.getElementById('sidebar').classList.contains('collapsed') ? 'Đã thu gọn sidebar.' : 'Đã mở sidebar.');
      });
    });
    
    </script>

    Metadata

    Metadata

    Assignees

    No one assigned

      Labels

      No labels
      No labels

      Type

      No type

      Projects

      No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions