451 lines
17 KiB
HTML
451 lines
17 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||
<title>GIS管网管理</title>
|
||
<!-- OpenLayers CSS -->
|
||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ol@v7.4.0/ol.css">
|
||
<style>
|
||
body, html { margin: 0; padding: 0; height: 100%; width: 100%; overflow: hidden; font-family: sans-serif; }
|
||
#map { width: 100%; height: 100%; }
|
||
|
||
/* UI Controls */
|
||
.ui-panel {
|
||
position: absolute;
|
||
top: 10px;
|
||
right: 10px;
|
||
background: rgba(255, 255, 255, 0.9);
|
||
padding: 10px;
|
||
border-radius: 4px;
|
||
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
|
||
z-index: 1000;
|
||
max-width: 200px;
|
||
}
|
||
.ui-group { margin-bottom: 10px; border-bottom: 1px solid #eee; padding-bottom: 5px; }
|
||
.ui-group:last-child { border-bottom: none; }
|
||
.ui-title { font-weight: bold; font-size: 14px; margin-bottom: 5px; }
|
||
button {
|
||
display: block;
|
||
width: 100%;
|
||
margin: 2px 0;
|
||
padding: 5px;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
background: #007bff;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 2px;
|
||
}
|
||
button:hover { background: #0056b3; }
|
||
button.active { background: #004494; border: 2px solid #002a5c; }
|
||
select { width: 100%; padding: 5px; margin-bottom: 5px; }
|
||
|
||
/* Popup */
|
||
.ol-popup {
|
||
position: absolute;
|
||
background-color: white;
|
||
box-shadow: 0 1px 4px rgba(0,0,0,0.2);
|
||
padding: 15px;
|
||
border-radius: 10px;
|
||
border: 1px solid #cccccc;
|
||
bottom: 12px;
|
||
left: -50px;
|
||
min-width: 200px;
|
||
}
|
||
.ol-popup:after, .ol-popup:before {
|
||
top: 100%;
|
||
border: solid transparent;
|
||
content: " ";
|
||
height: 0;
|
||
width: 0;
|
||
position: absolute;
|
||
pointer-events: none;
|
||
}
|
||
.ol-popup:after {
|
||
border-top-color: white;
|
||
border-width: 10px;
|
||
left: 48px;
|
||
margin-left: -10px;
|
||
}
|
||
.ol-popup:before {
|
||
border-top-color: #cccccc;
|
||
border-width: 11px;
|
||
left: 48px;
|
||
margin-left: -11px;
|
||
}
|
||
.ol-popup-closer {
|
||
text-decoration: none;
|
||
position: absolute;
|
||
top: 2px;
|
||
right: 8px;
|
||
color: #999;
|
||
}
|
||
.ol-popup-closer:after { content: "✖"; }
|
||
|
||
/* Legend */
|
||
.legend {
|
||
position: absolute;
|
||
bottom: 20px;
|
||
left: 10px;
|
||
background: rgba(255,255,255,0.8);
|
||
padding: 10px;
|
||
border-radius: 4px;
|
||
z-index: 1000;
|
||
font-size: 12px;
|
||
}
|
||
.legend-item { display: flex; align-items: center; margin-bottom: 5px; }
|
||
.color-box { width: 20px; height: 5px; margin-right: 5px; }
|
||
.point-circle { width: 10px; height: 10px; border-radius: 50%; margin-right: 5px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="map"></div>
|
||
|
||
<!-- Popup Overlay -->
|
||
<div id="popup" class="ol-popup">
|
||
<a href="#" id="popup-closer" class="ol-popup-closer"></a>
|
||
<div id="popup-content"></div>
|
||
</div>
|
||
|
||
<!-- UI Panel -->
|
||
<div class="ui-panel">
|
||
<div class="ui-group">
|
||
<div class="ui-title">底图切换</div>
|
||
<select id="baseLayerSelect" onchange="switchBaseLayer(this.value)">
|
||
<option value="OSM">OpenStreetMap</option>
|
||
<option value="GAODE">高德地图</option>
|
||
<option value="TIANDITU">天地图 (矢量)</option>
|
||
<option value="TIANDITU_IMG">天地图 (影像)</option>
|
||
</select>
|
||
</div>
|
||
<div class="ui-group">
|
||
<div class="ui-title">工具箱</div>
|
||
<button onclick="toggleMeasure('LineString')">测距</button>
|
||
<button onclick="toggleMeasure('Polygon')">测面</button>
|
||
<button onclick="clearMeasure()">清除测量</button>
|
||
</div>
|
||
<div class="ui-group">
|
||
<div class="ui-title">管网维护</div>
|
||
<button id="btn-add-pipe" onclick="startEdit('Pipe')">新增管线</button>
|
||
<button id="btn-add-station" onclick="startEdit('Station')">新增泵站</button>
|
||
<button onclick="stopEdit()">结束编辑</button>
|
||
<button onclick="deleteSelected()">删除选中</button>
|
||
</div>
|
||
<div class="ui-group">
|
||
<div class="ui-title">数据管理</div>
|
||
<button onclick="importCAD()">导入CAD (模拟)</button>
|
||
<button onclick="exportData()">导出数据</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="legend">
|
||
<div class="ui-title">图例</div>
|
||
<div class="legend-item"><div class="color-box" style="background:blue;"></div>供水管线</div>
|
||
<div class="legend-item"><div class="color-box" style="background:red;"></div>污水管线</div>
|
||
<div class="legend-item"><div class="point-circle" style="background:green;"></div>正常泵站</div>
|
||
<div class="legend-item"><div class="point-circle" style="background:gray;"></div>离线泵站</div>
|
||
</div>
|
||
|
||
<!-- OpenLayers JS -->
|
||
<script src="https://cdn.jsdelivr.net/npm/ol@v7.4.0/dist/ol.js"></script>
|
||
<script>
|
||
// --- Configuration ---
|
||
// Note: In a real app, API keys should be secure or proxy-based.
|
||
const TIANDITU_KEY = 'YOUR_TIANDITU_KEY_HERE'; // Replace with valid key
|
||
|
||
// --- Map Initialization ---
|
||
const map = new ol.Map({
|
||
target: 'map',
|
||
view: new ol.View({
|
||
center: ol.proj.fromLonLat([121.47, 31.23]), // Shanghai
|
||
zoom: 12
|
||
}),
|
||
controls: ol.control.defaults.defaults().extend([
|
||
new ol.control.ScaleLine(),
|
||
new ol.control.FullScreen()
|
||
])
|
||
});
|
||
|
||
// --- Base Layers ---
|
||
const baseLayers = {
|
||
'OSM': new ol.layer.Tile({
|
||
source: new ol.source.OSM(),
|
||
visible: true
|
||
}),
|
||
'GAODE': new ol.layer.Tile({
|
||
source: new ol.source.XYZ({
|
||
url: 'https://wprd0{1-4}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&style=7&x={x}&y={y}&z={z}'
|
||
}),
|
||
visible: false
|
||
}),
|
||
'TIANDITU': new ol.layer.Tile({
|
||
source: new ol.source.XYZ({
|
||
url: `http://t{0-7}.tianditu.gov.cn/DataServer?T=vec_w&x={x}&y={y}&l={z}&tk=${TIANDITU_KEY}`
|
||
}),
|
||
visible: false
|
||
}),
|
||
'TIANDITU_IMG': new ol.layer.Tile({
|
||
source: new ol.source.XYZ({
|
||
url: `http://t{0-7}.tianditu.gov.cn/DataServer?T=img_w&x={x}&y={y}&l={z}&tk=${TIANDITU_KEY}`
|
||
}),
|
||
visible: false
|
||
})
|
||
};
|
||
|
||
for (let key in baseLayers) {
|
||
map.addLayer(baseLayers[key]);
|
||
}
|
||
|
||
function switchBaseLayer(type) {
|
||
for (let key in baseLayers) {
|
||
baseLayers[key].setVisible(key === type);
|
||
}
|
||
}
|
||
|
||
// --- Business Layers (Pipe & Station) ---
|
||
const vectorSource = new ol.source.Vector();
|
||
const vectorLayer = new ol.layer.Vector({
|
||
source: vectorSource,
|
||
style: function(feature) {
|
||
const type = feature.get('type');
|
||
const status = feature.get('status');
|
||
|
||
if (type === 'Pipe') {
|
||
return new ol.style.Style({
|
||
stroke: new ol.style.Stroke({
|
||
color: feature.get('pipeType') === 'Sewage' ? 'red' : 'blue',
|
||
width: 3
|
||
})
|
||
});
|
||
} else if (type === 'Station') {
|
||
return new ol.style.Style({
|
||
image: new ol.style.Circle({
|
||
radius: 7,
|
||
fill: new ol.style.Fill({ color: status === 'Online' ? 'green' : 'gray' }),
|
||
stroke: new ol.style.Stroke({ color: 'white', width: 2 })
|
||
}),
|
||
text: new ol.style.Text({
|
||
text: feature.get('name'),
|
||
offsetY: -15,
|
||
fill: new ol.style.Fill({ color: '#000' }),
|
||
stroke: new ol.style.Stroke({ color: '#fff', width: 2 })
|
||
})
|
||
});
|
||
}
|
||
}
|
||
});
|
||
map.addLayer(vectorLayer);
|
||
|
||
// --- Mock Data ---
|
||
function loadMockData() {
|
||
// Pipe
|
||
const pipe = new ol.Feature({
|
||
geometry: new ol.geom.LineString([
|
||
ol.proj.fromLonLat([121.46, 31.23]),
|
||
ol.proj.fromLonLat([121.48, 31.23])
|
||
]),
|
||
type: 'Pipe',
|
||
pipeType: 'Water',
|
||
id: 'P001'
|
||
});
|
||
// Station
|
||
const station = new ol.Feature({
|
||
geometry: new ol.geom.Point(ol.proj.fromLonLat([121.47, 31.235])),
|
||
type: 'Station',
|
||
name: '1号泵站',
|
||
status: 'Online',
|
||
flow: '500m³/h',
|
||
level: '3.5m'
|
||
});
|
||
vectorSource.addFeatures([pipe, station]);
|
||
}
|
||
loadMockData();
|
||
|
||
// --- Interaction: Popup ---
|
||
const container = document.getElementById('popup');
|
||
const content = document.getElementById('popup-content');
|
||
const closer = document.getElementById('popup-closer');
|
||
|
||
const overlay = new ol.Overlay({
|
||
element: container,
|
||
autoPan: true,
|
||
autoPanAnimation: { duration: 250 }
|
||
});
|
||
map.addOverlay(overlay);
|
||
|
||
closer.onclick = function() {
|
||
overlay.setPosition(undefined);
|
||
closer.blur();
|
||
return false;
|
||
};
|
||
|
||
map.on('singleclick', function(evt) {
|
||
if (drawInteraction) return; // Don't popup when drawing
|
||
|
||
const feature = map.forEachFeatureAtPixel(evt.pixel, function(feature) {
|
||
return feature;
|
||
});
|
||
|
||
if (feature) {
|
||
const coordinate = evt.coordinate;
|
||
const type = feature.get('type');
|
||
let html = '';
|
||
if (type === 'Station') {
|
||
html = `<b>${feature.get('name')}</b><br>
|
||
状态: ${feature.get('status')}<br>
|
||
流量: ${feature.get('flow')}<br>
|
||
液位: ${feature.get('level')}`;
|
||
} else if (type === 'Pipe') {
|
||
html = `<b>管线 ID: ${feature.get('id')}</b><br>
|
||
类型: ${feature.get('pipeType') === 'Sewage' ? '污水' : '供水'}`;
|
||
}
|
||
content.innerHTML = html;
|
||
overlay.setPosition(coordinate);
|
||
} else {
|
||
overlay.setPosition(undefined);
|
||
}
|
||
});
|
||
|
||
// --- Interaction: Draw & Modify ---
|
||
let drawInteraction;
|
||
const modifyInteraction = new ol.interaction.Modify({ source: vectorSource });
|
||
const selectInteraction = new ol.interaction.Select();
|
||
|
||
map.addInteraction(modifyInteraction);
|
||
map.addInteraction(selectInteraction);
|
||
|
||
function startEdit(type) {
|
||
stopEdit(); // Clear previous
|
||
|
||
let geometryType = type === 'Station' ? 'Point' : 'LineString';
|
||
drawInteraction = new ol.interaction.Draw({
|
||
source: vectorSource,
|
||
type: geometryType
|
||
});
|
||
|
||
drawInteraction.on('drawend', function(evt) {
|
||
const feature = evt.feature;
|
||
feature.set('type', type);
|
||
if (type === 'Station') {
|
||
feature.set('name', '新泵站');
|
||
feature.set('status', 'Online');
|
||
} else {
|
||
feature.set('pipeType', 'Water');
|
||
feature.set('id', 'New-' + Date.now());
|
||
}
|
||
});
|
||
|
||
map.addInteraction(drawInteraction);
|
||
|
||
// UI feedback
|
||
document.querySelectorAll('button').forEach(b => b.classList.remove('active'));
|
||
if(type === 'Pipe') document.getElementById('btn-add-pipe').classList.add('active');
|
||
if(type === 'Station') document.getElementById('btn-add-station').classList.add('active');
|
||
}
|
||
|
||
function stopEdit() {
|
||
if (drawInteraction) {
|
||
map.removeInteraction(drawInteraction);
|
||
drawInteraction = null;
|
||
}
|
||
document.querySelectorAll('button').forEach(b => b.classList.remove('active'));
|
||
}
|
||
|
||
function deleteSelected() {
|
||
const selectedFeatures = selectInteraction.getFeatures();
|
||
selectedFeatures.forEach(function(feature) {
|
||
vectorSource.removeFeature(feature);
|
||
});
|
||
selectedFeatures.clear();
|
||
overlay.setPosition(undefined);
|
||
}
|
||
|
||
// --- Tools: Measure ---
|
||
let measureDraw;
|
||
function toggleMeasure(type) {
|
||
stopEdit();
|
||
if (measureDraw) map.removeInteraction(measureDraw);
|
||
|
||
measureDraw = new ol.interaction.Draw({
|
||
source: new ol.source.Vector(),
|
||
type: type
|
||
});
|
||
|
||
measureDraw.on('drawstart', function(evt) {
|
||
// Simplified measurement logic for demo
|
||
// In real app, create tooltips and calculate length/area using ol.sphere
|
||
});
|
||
|
||
measureDraw.on('drawend', function(evt) {
|
||
const geom = evt.feature.getGeometry();
|
||
let output;
|
||
if (geom instanceof ol.geom.Polygon) {
|
||
output = '面积: ' + (geom.getArea() / 1000000).toFixed(2) + ' km²';
|
||
} else if (geom instanceof ol.geom.LineString) {
|
||
output = '距离: ' + (geom.getLength() / 1000).toFixed(2) + ' km';
|
||
}
|
||
alert(output);
|
||
map.removeInteraction(measureDraw); // Stop after one measure
|
||
});
|
||
|
||
map.addInteraction(measureDraw);
|
||
}
|
||
|
||
function clearMeasure() {
|
||
// Logic to clear measurement overlays would go here
|
||
if (measureDraw) map.removeInteraction(measureDraw);
|
||
}
|
||
|
||
// --- Data Import/Export ---
|
||
function importCAD() {
|
||
// Mock implementation
|
||
// In reality, this would trigger a file input, upload to server,
|
||
// server converts DWG -> GeoJSON, returns GeoJSON, then:
|
||
// const format = new ol.format.GeoJSON();
|
||
// const features = format.readFeatures(jsonData);
|
||
// vectorSource.addFeatures(features);
|
||
alert("模拟导入CAD图纸成功!已转换为GeoJSON并加载。");
|
||
|
||
// Add a mock feature representing CAD import
|
||
const cadFeature = new ol.Feature({
|
||
geometry: new ol.geom.LineString([
|
||
ol.proj.fromLonLat([121.465, 31.225]),
|
||
ol.proj.fromLonLat([121.475, 31.225])
|
||
]),
|
||
type: 'Pipe',
|
||
pipeType: 'Sewage',
|
||
id: 'CAD-001'
|
||
});
|
||
vectorSource.addFeature(cadFeature);
|
||
}
|
||
|
||
function exportData() {
|
||
const format = new ol.format.GeoJSON();
|
||
const features = vectorSource.getFeatures();
|
||
const json = format.writeFeatures(features);
|
||
console.log(json);
|
||
alert("数据已导出 (查看控制台)");
|
||
// Can implement file download here
|
||
}
|
||
|
||
// --- Android Interface ---
|
||
// If loaded in Android WebView, this allows Android to call JS
|
||
window.locateDevice = function(lat, lon) {
|
||
const coords = ol.proj.fromLonLat([lon, lat]);
|
||
map.getView().animate({ center: coords, zoom: 15 });
|
||
|
||
// Add marker for user location
|
||
const userFeature = new ol.Feature({
|
||
geometry: new ol.geom.Point(coords),
|
||
type: 'Station', // Reuse station style or define new
|
||
name: '当前位置',
|
||
status: 'Online'
|
||
});
|
||
vectorSource.addFeature(userFeature);
|
||
};
|
||
|
||
</script>
|
||
</body>
|
||
</html>
|