-
Notifications
You must be signed in to change notification settings - Fork 1
Open
Description
<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>
<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();
}
</script>
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
Dữ liệu
// 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.');
});
});
Metadata
Metadata
Assignees
Labels
No labels