Pythoner 的Flask项目实践-添加Shapefile面数据并展示功能Mapboxgl底图
文章目录
- 1,实现思路
- 2,环境依赖
- 3,实现步骤
- 3.1. Flask 后端(app.py)
- 3.2. 前端 templates/addshpfile.html
- 3.3.属性表查看(点击面弹出属性字段 Popup) 的功能
- 4,效果展示
在上一篇的项目中,我们添加手动选择本地shpfile文件并压缩上传展示在mapboxgl底图上。
1,实现思路
-
后端 Flask:用 geopandas 读取 shp 文件,转成 GeoJSON。
-
前端 HTML/JS:用Mapboxgl地图库把 GeoJSON 渲染出来。
-
自动缩放到面数据范围。
-
新建路由 /map:显示地图页面。
2,环境依赖
在 web_demo conda 环境里安装:
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)
from flask import Flask, render_template, request, redirect, url_for, jsonify
import geopandas as gpd
import os, zipfile, tempfile, shutilapp = Flask(__name__)# 内存中的签到列表
sign_in_list = []@app.route("/")
def home():"""首页"""return render_template("home.html")@app.route("/signwallet", methods=["GET", "POST"])
def signwallet():"""首页:签到表单 + 显示签到结果"""message = Noneif 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": "没有检测到文件"}, 400file = 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 = Nonefor f in os.listdir(tmp_dir):if f.endswith(".shp"):shp_file = os.path.join(tmp_dir, f)breakif not shp_file:shutil.rmtree(tmp_dir)return {"error": "ZIP 文件里没有找到 .shp"}, 400# 读取并转成 GeoJSONtry: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
{% 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 Tokenmapboxgl.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添加以下代码
// ✅ 点击显示属性表(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 显示面数据
-
鼠标点击某个面 → 弹窗显示该要素的所有属性字段和值
-
鼠标悬停时变成小手,提示可点击
“人的一生会经历很多痛苦,但回头想想,都是传奇”。