文章目录
在上一篇的项目中,我们添加手动选择本地shpfile文件并压缩上传展示在mapboxgl底图上。
1,实现思路
-
后端 Flask:用 geopandas 读取 shp 文件,转成 GeoJSON。
-
前端 HTML/JS:用Mapboxgl地图库把 GeoJSON 渲染出来。
-
自动缩放到面数据范围。
-
新建路由 /map:显示地图页面。
2,环境依赖
在 web_demo conda 环境里安装:
bash
conda install geopandas shapely fiona pyproj -c conda-forge

3,实现步骤
因为Shapefile 必须包含 至少 3 个文件(.shp, .shx, .dbf),通常还可能有 .prj;所以上传时最好打包成 .zip,这样更方便后端解压读取。
具体实现功能:点击按钮 → 弹窗选择本地 .shp 文件 → 上传到 Flask → 在 MapboxGL 里加载显示。
3.1. Flask 后端(app.py)
python
from flask import Flask, render_template, request, redirect, url_for, jsonify
import geopandas as gpd
import os, zipfile, tempfile, shutil
app = Flask(__name__)
# 内存中的签到列表
sign_in_list = []
@app.route("/")
def home():
"""首页"""
return render_template("home.html")
@app.route("/signwallet", methods=["GET", "POST"])
def signwallet():
"""首页:签到表单 + 显示签到结果"""
message = None
if request.method == "POST":
name = request.form.get("username", "").strip()
if name:
if name not in sign_in_list:
sign_in_list.append(name)
message = f"欢迎你,{name}!"
else:
message = "请输入名字再提交哦~"
return render_template("signwallet.html", message=message, users=sign_in_list)
@app.route("/about")
def about():
"""关于页面"""
return render_template("about.html")
@app.route("/contact")
def contact():
"""联系我们页面"""
return render_template("contact.html")
@app.route("/updatefeature")
def updatefeature():
"""联系我们页面"""
return render_template("updatefeature.html")
@app.route("/terrain3D")
def terrain3D():
"""联系我们页面"""
return render_template("terrain3D.html")
@app.route("/clear")
def clear():
"""清空签到列表后跳回首页"""
sign_in_list.clear()
return redirect(url_for("home"))
# 新增:地图页面
@app.route("/addshpfile")
def addshpfile():
return render_template("addshpfile.html")
# 提供 GeoJSON 数据接口
@app.route("/data/shapefile")
def shapefile_data():
shp_path = "../web_demo_flask/data/project_boundary1.shp" # 放在项目 data/ 目录下
gdf = gpd.read_file(shp_path)
gdf = gdf.to_crs(epsg=4326) # 转WGS84经纬度,Mapbox才能正确显示
# return jsonify(gdf.to_crs(epsg=4326).__geo_interface__) # 转WGS84,前端能识别
return jsonify(gdf.__geo_interface__) # 转GeoJSON输出
# 上传 shapefile (zip 格式)
@app.route("/upload_shp", methods=["POST"])
def upload_shp():
if "file" not in request.files:
return {"error": "没有检测到文件"}, 400
file = request.files["file"]
if not file.filename.endswith(".zip"):
return {"error": "请上传包含 .shp/.dbf/.shx 的 ZIP 文件"}, 400
# 保存临时文件
tmp_dir = tempfile.mkdtemp()
zip_path = os.path.join(tmp_dir, file.filename)
file.save(zip_path)
# 解压
with zipfile.ZipFile(zip_path, "r") as zip_ref:
zip_ref.extractall(tmp_dir)
# 找到 .shp 文件
shp_file = None
for f in os.listdir(tmp_dir):
if f.endswith(".shp"):
shp_file = os.path.join(tmp_dir, f)
break
if not shp_file:
shutil.rmtree(tmp_dir)
return {"error": "ZIP 文件里没有找到 .shp"}, 400
# 读取并转成 GeoJSON
try:
gdf = gpd.read_file(shp_file).to_crs(epsg=4326)
geojson = gdf.__geo_interface__
except Exception as e:
shutil.rmtree(tmp_dir)
return {"error": str(e)}, 500
# 清理临时目录
shutil.rmtree(tmp_dir)
return jsonify(geojson)
if __name__ == "__main__":
app.run(debug=True, port=5000)
3.2. 前端 templates/addshpfile.html
html
{% extends "home.html" %}
{% block title %}地图展示{% endblock %}
{% block content %}
<!-- Mapbox GL JS -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Shapefile 面数据展示 (Mapbox GL)</title>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no">
<link href="https://api.mapbox.com/mapbox-gl-js/v3.15.0/mapbox-gl.css" rel="stylesheet">
<script src="https://api.mapbox.com/mapbox-gl-js/v3.15.0/mapbox-gl.js"></script>
<style>
body { margin: 0; padding: 0; }
#map { position: absolute; top: 100px; bottom: 0; width: 100%; }
</style>
</head>
<body>
<h3>上传 Shapefile (ZIP)</h3>
<form id="uploadForm">
<input type="file" id="shpFile" accept=".zip" />
<button type="submit">上传并显示</button>
</form>
<div id="map" style="height: 500px; width: 100%; margin-top:10px;"></div>
<script>
// 替换成你自己的 Mapbox Access Token
mapboxgl.accessToken = 'pk.eyJ1IjoidGlnZXJiZ3AyMDIwIiwiYSI6ImNsaGhpb3Q0ZTBvMWEzcW1xcXd4aTk5bzIifQ.4mA7mUrhK09N4vrrQfZA_Q';
var map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: [54.5, 24.0], // 阿联酋大致中心
zoom: 6
});
document.getElementById("uploadForm").addEventListener("submit", function(e){
e.preventDefault();
var fileInput = document.getElementById("shpFile");
if(fileInput.files.length === 0){
alert("请选择一个 zip 文件");
return;
}
var formData = new FormData();
formData.append("file", fileInput.files[0]);
fetch("/upload_shp", {
method: "POST",
body: formData
})
.then(res => res.json())
.then(data => {
if(data.error){
alert("错误: " + data.error);
return;
}
// 如果已有图层,先移除
if(map.getSource("uploadedShp")){
map.removeLayer("uploadedShp-fill");
map.removeLayer("uploadedShp-line");
map.removeSource("uploadedShp");
}
map.addSource("uploadedShp", {
"type": "geojson",
"data": data
});
map.addLayer({
"id": "uploadedShp-fill",
"type": "fill",
"source": "uploadedShp",
"paint": {"fill-color": "#088", "fill-opacity": 0.5}
});
map.addLayer({
"id": "uploadedShp-line",
"type": "line",
"source": "uploadedShp",
"paint": {"line-color": "#000", "line-width": 2}
});
// 自动缩放
var bbox = turf.bbox(data);
map.fitBounds(bbox, {padding: 20});
// 点击显示属性表(Popup)
map.on("click", "uploadedShp-fill", function(e){
var props = e.features[0].properties;
var html = "<b>属性信息:</b><br>";
for(var key in props){
html += key + ": " + props[key] + "<br>";
}
new mapboxgl.Popup()
.setLngLat(e.lngLat)
.setHTML(html)
.addTo(map);
});
// 鼠标悬停时变成小手
map.on("mouseenter", "uploadedShp-fill", function () {
map.getCanvas().style.cursor = "pointer";
});
map.on("mouseleave", "uploadedShp-fill", function () {
map.getCanvas().style.cursor = "";
});
})
.catch(err => alert("上传失败: " + err));
});
</script>
<!-- Turf.js 用于计算 bbox -->
<script src="https://cdn.jsdelivr.net/npm/@turf/turf@6/turf.min.js"></script>
{% endblock %}
使用方法:
-
把 .shp/.shx/.dbf/.prj 打包成一个 yourdata.zip
-
在页面点击 选择文件 → 选 yourdata.zip → 点击上传
-
Flask 解压并读取 shapefile → 返回 GeoJSON → MapboxGL 渲染
3.3.属性表查看(点击面弹出属性字段 Popup) 的功能
- Flask 后端(无需改动)
之前 /upload_shp 返回的 geojson 已经包含了属性字段(properties),直接在前端用就行。
- 前端html添加以下代码
python
// ✅ 点击显示属性表(Popup)
map.on("click", "uploadedShp-fill", function(e){
var props = e.features[0].properties;
var html = "<b>属性信息:</b><br>";
for(var key in props){
html += key + ": " + props[key] + "<br>";
}
new mapboxgl.Popup()
.setLngLat(e.lngLat)
.setHTML(html)
.addTo(map);
});
// 鼠标悬停时变成小手
map.on("mouseenter", "uploadedShp-fill", function () {
map.getCanvas().style.cursor = "pointer";
});
map.on("mouseleave", "uploadedShp-fill", function () {
map.getCanvas().style.cursor = "";
});
4,效果展示
-
上传 .shp + .shx + .dbf 文件的压缩zip文件 → MapboxGL 显示面数据
-
鼠标点击某个面 → 弹窗显示该要素的所有属性字段和值
-
鼠标悬停时变成小手,提示可点击


"人的一生会经历很多痛苦,但回头想想,都是传奇"。