Pythoner 的Flask项目实践-添加Shapefile面数据并展示功能Mapboxgl底图

文章目录

在上一篇的项目中,我们添加手动选择本地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 %}

使用方法:

  1. 把 .shp/.shx/.dbf/.prj 打包成一个 yourdata.zip

  2. 在页面点击 选择文件 → 选 yourdata.zip → 点击上传

  3. Flask 解压并读取 shapefile → 返回 GeoJSON → MapboxGL 渲染

3.3.属性表查看(点击面弹出属性字段 Popup) 的功能

  1. Flask 后端(无需改动)

之前 /upload_shp 返回的 geojson 已经包含了属性字段(properties),直接在前端用就行。

  1. 前端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,效果展示

  1. 上传 .shp + .shx + .dbf 文件的压缩zip文件 → MapboxGL 显示面数据

  2. 鼠标点击某个面 → 弹窗显示该要素的所有属性字段和值

  3. 鼠标悬停时变成小手,提示可点击


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


相关推荐
Q_Q19632884755 小时前
python+django/flask二手物品交易系统 二手商品发布 分类浏览 在线沟通与订单管理系统java+nodejs
java·spring boot·python·django·flask·node.js·php
小闫BI设源码5 小时前
Python Flask快速入门
开发语言·python·flask
冷小鱼7 小时前
[BUG]MarkupSafe==3.0.2
flask·bug·markupsafe
面向星辰8 小时前
flask部署服务器允许其他电脑访问
服务器·python·flask
RTOS_Runaway_Robot8 小时前
基于Pycharm的Python-flask 的学习分享 04
python·pycharm·flask
Q_Q5110082858 小时前
python+springboot+uniapp微信小程序“美好食荐”系统 美食推荐 菜谱展示 用户互动 评论收藏系统
spring boot·python·微信小程序·django·flask·uni-app·node.js
Q_Q51100828514 小时前
python+springboot毕业季旅游一站式定制服务系统
java·spring boot·python·django·flask·node.js·旅游
yzx99101318 小时前
对比django,flask,opencv三大
人工智能·后端·python·django·flask
Q_Q51100828519 小时前
python+springboot+django/flask的医院食堂订餐系统 菜单发布 在线订餐 餐品管理与订单统计系统
spring boot·python·django·flask·node.js·php