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>
|